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.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.github/workflows/deploy-yard-docs.yml +52 -0
- data/CHANGELOG.md +55 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +332 -0
- data/Rakefile +67 -0
- data/docs/api/adapters/anthropic.md +121 -0
- data/docs/api/adapters/gemini.md +133 -0
- data/docs/api/adapters/index.md +104 -0
- data/docs/api/adapters/openai.md +134 -0
- data/docs/api/core/index.md +113 -0
- data/docs/api/core/memory.md +314 -0
- data/docs/api/core/network.md +291 -0
- data/docs/api/core/robot.md +273 -0
- data/docs/api/core/state.md +273 -0
- data/docs/api/core/tool.md +353 -0
- data/docs/api/history/active-record-adapter.md +195 -0
- data/docs/api/history/config.md +191 -0
- data/docs/api/history/index.md +132 -0
- data/docs/api/history/thread-manager.md +144 -0
- data/docs/api/index.md +82 -0
- data/docs/api/mcp/client.md +221 -0
- data/docs/api/mcp/index.md +111 -0
- data/docs/api/mcp/server.md +225 -0
- data/docs/api/mcp/transports.md +264 -0
- data/docs/api/messages/index.md +67 -0
- data/docs/api/messages/text-message.md +102 -0
- data/docs/api/messages/tool-call-message.md +144 -0
- data/docs/api/messages/tool-result-message.md +154 -0
- data/docs/api/messages/user-message.md +171 -0
- data/docs/api/streaming/context.md +174 -0
- data/docs/api/streaming/events.md +237 -0
- data/docs/api/streaming/index.md +108 -0
- data/docs/architecture/core-concepts.md +243 -0
- data/docs/architecture/index.md +138 -0
- data/docs/architecture/message-flow.md +320 -0
- data/docs/architecture/network-orchestration.md +216 -0
- data/docs/architecture/robot-execution.md +243 -0
- data/docs/architecture/state-management.md +323 -0
- data/docs/assets/css/custom.css +56 -0
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +216 -0
- data/docs/examples/basic-chat.md +193 -0
- data/docs/examples/index.md +129 -0
- data/docs/examples/mcp-server.md +290 -0
- data/docs/examples/multi-robot-network.md +312 -0
- data/docs/examples/rails-application.md +420 -0
- data/docs/examples/tool-usage.md +310 -0
- data/docs/getting-started/configuration.md +230 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +179 -0
- data/docs/getting-started/quick-start.md +203 -0
- data/docs/guides/building-robots.md +376 -0
- data/docs/guides/creating-networks.md +366 -0
- data/docs/guides/history.md +359 -0
- data/docs/guides/index.md +68 -0
- data/docs/guides/mcp-integration.md +356 -0
- data/docs/guides/memory.md +309 -0
- data/docs/guides/rails-integration.md +432 -0
- data/docs/guides/streaming.md +314 -0
- data/docs/guides/using-tools.md +394 -0
- data/docs/index.md +160 -0
- data/examples/01_simple_robot.rb +38 -0
- data/examples/02_tools.rb +106 -0
- data/examples/03_network.rb +103 -0
- data/examples/04_mcp.rb +219 -0
- data/examples/05_streaming.rb +124 -0
- data/examples/06_prompt_templates.rb +324 -0
- data/examples/07_network_memory.rb +329 -0
- data/examples/prompts/assistant/system.txt.erb +2 -0
- data/examples/prompts/assistant/user.txt.erb +1 -0
- data/examples/prompts/billing/system.txt.erb +7 -0
- data/examples/prompts/billing/user.txt.erb +1 -0
- data/examples/prompts/classifier/system.txt.erb +4 -0
- data/examples/prompts/classifier/user.txt.erb +1 -0
- data/examples/prompts/entity_extractor/system.txt.erb +11 -0
- data/examples/prompts/entity_extractor/user.txt.erb +3 -0
- data/examples/prompts/escalation/system.txt.erb +35 -0
- data/examples/prompts/escalation/user.txt.erb +34 -0
- data/examples/prompts/general/system.txt.erb +4 -0
- data/examples/prompts/general/user.txt.erb +1 -0
- data/examples/prompts/github_assistant/system.txt.erb +6 -0
- data/examples/prompts/github_assistant/user.txt.erb +1 -0
- data/examples/prompts/helper/system.txt.erb +1 -0
- data/examples/prompts/helper/user.txt.erb +1 -0
- data/examples/prompts/keyword_extractor/system.txt.erb +8 -0
- data/examples/prompts/keyword_extractor/user.txt.erb +3 -0
- data/examples/prompts/order_support/system.txt.erb +27 -0
- data/examples/prompts/order_support/user.txt.erb +22 -0
- data/examples/prompts/product_support/system.txt.erb +30 -0
- data/examples/prompts/product_support/user.txt.erb +32 -0
- data/examples/prompts/sentiment_analyzer/system.txt.erb +9 -0
- data/examples/prompts/sentiment_analyzer/user.txt.erb +3 -0
- data/examples/prompts/synthesizer/system.txt.erb +14 -0
- data/examples/prompts/synthesizer/user.txt.erb +15 -0
- data/examples/prompts/technical/system.txt.erb +7 -0
- data/examples/prompts/technical/user.txt.erb +1 -0
- data/examples/prompts/triage/system.txt.erb +16 -0
- data/examples/prompts/triage/user.txt.erb +17 -0
- data/lib/generators/robot_lab/install_generator.rb +78 -0
- data/lib/generators/robot_lab/robot_generator.rb +55 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +41 -0
- data/lib/generators/robot_lab/templates/migration.rb.tt +32 -0
- data/lib/generators/robot_lab/templates/result_model.rb.tt +52 -0
- data/lib/generators/robot_lab/templates/robot.rb.tt +46 -0
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +32 -0
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +53 -0
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +40 -0
- data/lib/robot_lab/adapters/anthropic.rb +163 -0
- data/lib/robot_lab/adapters/base.rb +85 -0
- data/lib/robot_lab/adapters/gemini.rb +193 -0
- data/lib/robot_lab/adapters/openai.rb +159 -0
- data/lib/robot_lab/adapters/registry.rb +81 -0
- data/lib/robot_lab/configuration.rb +143 -0
- data/lib/robot_lab/error.rb +32 -0
- data/lib/robot_lab/errors.rb +70 -0
- data/lib/robot_lab/history/active_record_adapter.rb +146 -0
- data/lib/robot_lab/history/config.rb +115 -0
- data/lib/robot_lab/history/thread_manager.rb +93 -0
- data/lib/robot_lab/mcp/client.rb +210 -0
- data/lib/robot_lab/mcp/server.rb +84 -0
- data/lib/robot_lab/mcp/transports/base.rb +56 -0
- data/lib/robot_lab/mcp/transports/sse.rb +117 -0
- data/lib/robot_lab/mcp/transports/stdio.rb +133 -0
- data/lib/robot_lab/mcp/transports/streamable_http.rb +139 -0
- data/lib/robot_lab/mcp/transports/websocket.rb +108 -0
- data/lib/robot_lab/memory.rb +882 -0
- data/lib/robot_lab/memory_change.rb +123 -0
- data/lib/robot_lab/message.rb +357 -0
- data/lib/robot_lab/network.rb +350 -0
- data/lib/robot_lab/rails/engine.rb +29 -0
- data/lib/robot_lab/rails/railtie.rb +42 -0
- data/lib/robot_lab/robot.rb +560 -0
- data/lib/robot_lab/robot_result.rb +205 -0
- data/lib/robot_lab/robotic_model.rb +324 -0
- data/lib/robot_lab/state_proxy.rb +188 -0
- data/lib/robot_lab/streaming/context.rb +144 -0
- data/lib/robot_lab/streaming/events.rb +95 -0
- data/lib/robot_lab/streaming/sequence_counter.rb +48 -0
- data/lib/robot_lab/task.rb +117 -0
- data/lib/robot_lab/tool.rb +223 -0
- data/lib/robot_lab/tool_config.rb +112 -0
- data/lib/robot_lab/tool_manifest.rb +234 -0
- data/lib/robot_lab/user_message.rb +118 -0
- data/lib/robot_lab/version.rb +5 -0
- data/lib/robot_lab/waiter.rb +73 -0
- data/lib/robot_lab.rb +195 -0
- data/mkdocs.yml +214 -0
- data/sig/robot_lab.rbs +4 -0
- metadata +442 -0
data/README.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# RobotLab
|
|
2
|
+
|
|
3
|
+
> [!CAUTION]
|
|
4
|
+
> This gem is under active development. APIs and features may change without notice. See the [CHANGELOG](CHANGELOG.md) for details.
|
|
5
|
+
<br>
|
|
6
|
+
<table>
|
|
7
|
+
<tr>
|
|
8
|
+
<td width="50%" align="center" valign="top">
|
|
9
|
+
<img src="docs/assets/images/robot_lab.jpg" alt="RobotLab"><br>
|
|
10
|
+
<em>"Build robots. Solve problems."</em>
|
|
11
|
+
</td>
|
|
12
|
+
<td width="50%" valign="top">
|
|
13
|
+
<strong>Multi-robot LLM workflow orchestration for Ruby</strong><br><br>
|
|
14
|
+
RobotLab enables you to build sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own system prompt, tools, and capabilities.<br><br>
|
|
15
|
+
<strong>Key Features</strong><br>
|
|
16
|
+
|
|
17
|
+
- <strong>Multi-Robot Architecture</strong> - Build with specialized AI agents<br>
|
|
18
|
+
- <strong>Network Orchestration</strong> - Connect robots with flexible routing<br>
|
|
19
|
+
- <strong>Extensible Tools</strong> - Give robots custom capabilities<br>
|
|
20
|
+
- <strong>MCP Integration</strong> - Connect to external tool servers<br>
|
|
21
|
+
- <strong>Shared Memory</strong> - Hierarchical memory with namespaced scopes<br>
|
|
22
|
+
- <strong>Conversation History</strong> - Persist and restore threads<br>
|
|
23
|
+
- <strong>Streaming</strong> - Real-time event streaming<br>
|
|
24
|
+
- <strong>Rails Integration</strong> - Generators and ActiveRecord support
|
|
25
|
+
</td>
|
|
26
|
+
</tr>
|
|
27
|
+
</table>
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bundle add robot_lab
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or install it directly:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gem install robot_lab
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Requirements
|
|
42
|
+
|
|
43
|
+
- Ruby >= 3.2
|
|
44
|
+
- [One or more API Keys for LLM providers supported by RubyLLM](https://rubyllm.com/configuration/#provider-configuration)
|
|
45
|
+
|
|
46
|
+
For comprehensive guides and API documentation, visit **[https://madbomber.github.io/robot_lab](https://madbomber.github.io/robot_lab)**
|
|
47
|
+
|
|
48
|
+
## Getting Started
|
|
49
|
+
|
|
50
|
+
The simplest way to create a robot is with an inline `system_prompt`. This approach is ideal for development, testing, and quick prototyping:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
require "robot_lab"
|
|
54
|
+
|
|
55
|
+
# Configure RobotLab
|
|
56
|
+
RobotLab.configure do |config|
|
|
57
|
+
config.default_model = "claude-sonnet-4"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Create a robot with an inline system prompt
|
|
61
|
+
robot = RobotLab.build(
|
|
62
|
+
name: "assistant",
|
|
63
|
+
system_prompt: "You are a helpful assistant. Be concise and friendly."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Run the robot
|
|
67
|
+
result = robot.run(message: "What is the capital of France?")
|
|
68
|
+
|
|
69
|
+
puts result.output.first.content
|
|
70
|
+
# => "The capital of France is Paris."
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Using Templates
|
|
74
|
+
|
|
75
|
+
For production applications, RobotLab supports a powerful template system built on ERB. Templates allow you to:
|
|
76
|
+
|
|
77
|
+
- **Compose prompts** from reusable components
|
|
78
|
+
- **Inject dynamic context** at build-time and run-time
|
|
79
|
+
- **Version control** your prompts alongside your code
|
|
80
|
+
- **Share prompts** across multiple robots
|
|
81
|
+
|
|
82
|
+
Configure the template directory:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
RobotLab.configure do |config|
|
|
86
|
+
config.template_path = "app/prompts"
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Each template is a **directory** containing ERB files for different message roles:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
app/prompts/
|
|
94
|
+
assistant/
|
|
95
|
+
├── system.txt.erb # System message (required)
|
|
96
|
+
├── user.txt.erb # User prompt template (optional)
|
|
97
|
+
├── assistant.txt.erb # Pre-filled assistant response (optional)
|
|
98
|
+
└── schema.rb # Structured output schema (optional)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Create the system message at `app/prompts/assistant/system.txt.erb`:
|
|
102
|
+
|
|
103
|
+
```erb
|
|
104
|
+
You are a helpful assistant for <%= company_name %>.
|
|
105
|
+
|
|
106
|
+
Your responsibilities:
|
|
107
|
+
- Answer questions accurately and concisely
|
|
108
|
+
- Be friendly and professional
|
|
109
|
+
- Admit when you don't know something
|
|
110
|
+
|
|
111
|
+
<% if guidelines %>
|
|
112
|
+
Additional guidelines:
|
|
113
|
+
<%= guidelines %>
|
|
114
|
+
<% end %>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Reference the template directory using a Symbol:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
robot = RobotLab.build(
|
|
121
|
+
name: "assistant",
|
|
122
|
+
template: :assistant,
|
|
123
|
+
context: { company_name: "Acme Corp", guidelines: nil }
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Combining Templates with System Prompts
|
|
128
|
+
|
|
129
|
+
The `system_prompt` parameter can also be used alongside a template. When both are provided, the template renders first and the `system_prompt` is appended. This is particularly useful during development and testing when you want to add temporary instructions or context to an existing template:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
robot = RobotLab.build(
|
|
133
|
+
name: "assistant",
|
|
134
|
+
template: :assistant,
|
|
135
|
+
context: { company_name: "Acme Corp" },
|
|
136
|
+
system_prompt: "DEBUG MODE: Log all tool calls. Today's date is #{Date.today}."
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Creating a Robot with Tools
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# Define tools using RubyLLM::Tool
|
|
144
|
+
class Magic8Ball < RubyLLM::Tool
|
|
145
|
+
description "Consult the mystical Magic 8-Ball for guidance on yes/no questions"
|
|
146
|
+
|
|
147
|
+
param :question, type: "string", desc: "A yes/no question to ask the oracle"
|
|
148
|
+
|
|
149
|
+
RESPONSES = [
|
|
150
|
+
{ answer: "It is certain", certainty: 0.95, vibe: "positive" },
|
|
151
|
+
{ answer: "Ask again later", certainty: 0.10, vibe: "evasive" },
|
|
152
|
+
{ answer: "Don't count on it", certainty: 0.85, vibe: "negative" },
|
|
153
|
+
{ answer: "Signs point to yes", certainty: 0.75, vibe: "positive" },
|
|
154
|
+
{ answer: "Reply hazy, try again", certainty: 0.05, vibe: "evasive" },
|
|
155
|
+
{ answer: "My sources say no", certainty: 0.80, vibe: "negative" },
|
|
156
|
+
{ answer: "Outlook good", certainty: 0.70, vibe: "positive" },
|
|
157
|
+
{ answer: "Cannot predict now", certainty: 0.00, vibe: "evasive" }
|
|
158
|
+
].freeze
|
|
159
|
+
|
|
160
|
+
def execute(question:)
|
|
161
|
+
response = RESPONSES.sample
|
|
162
|
+
{ question: question, **response }
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Create robot with tools
|
|
167
|
+
robot = RobotLab.build(
|
|
168
|
+
name: "oracle",
|
|
169
|
+
system_prompt: "You are a mystical oracle. Use the Magic 8-Ball to answer questions about the future.",
|
|
170
|
+
tools: [Magic8Ball]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
result = robot.run(message: "Should I start learning Rust?")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Orchestrating Multiple Robots
|
|
177
|
+
|
|
178
|
+
Networks use [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines with optional task activation for intelligent routing:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
# Custom classifier that activates the appropriate specialist
|
|
182
|
+
class ClassifierRobot < RobotLab::Robot
|
|
183
|
+
def call(result)
|
|
184
|
+
robot_result = run(**extract_run_context(result))
|
|
185
|
+
|
|
186
|
+
new_result = result
|
|
187
|
+
.with_context(@name.to_sym, robot_result)
|
|
188
|
+
.continue(robot_result)
|
|
189
|
+
|
|
190
|
+
# Route based on classification
|
|
191
|
+
category = robot_result.last_text_content.to_s.strip.downcase
|
|
192
|
+
case category
|
|
193
|
+
when /billing/ then new_result.activate(:billing)
|
|
194
|
+
when /technical/ then new_result.activate(:technical)
|
|
195
|
+
else new_result.activate(:general)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Create specialized robots
|
|
201
|
+
classifier = ClassifierRobot.new(
|
|
202
|
+
name: "classifier",
|
|
203
|
+
template: :classifier
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
billing_robot = RobotLab.build(name: "billing", template: :billing)
|
|
207
|
+
technical_robot = RobotLab.build(name: "technical", template: :technical)
|
|
208
|
+
general_robot = RobotLab.build(name: "general", template: :general)
|
|
209
|
+
|
|
210
|
+
# Create network with optional task routing
|
|
211
|
+
network = RobotLab.create_network(name: "support") do
|
|
212
|
+
task :classifier, classifier, depends_on: :none
|
|
213
|
+
task :billing, billing_robot, depends_on: :optional
|
|
214
|
+
task :technical, technical_robot, depends_on: :optional
|
|
215
|
+
task :general, general_robot, depends_on: :optional
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Run the network
|
|
219
|
+
result = network.run(message: "I was charged twice for my subscription")
|
|
220
|
+
puts result.value.last_text_content
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Memory
|
|
224
|
+
|
|
225
|
+
Both robots and networks have inherent memory that persists across runs:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# Standalone robot with inherent memory
|
|
229
|
+
robot = RobotLab.build(name: "assistant", system_prompt: "You are helpful.")
|
|
230
|
+
|
|
231
|
+
robot.run(message: "My name is Alice")
|
|
232
|
+
robot.run(message: "What's my name?") # Memory persists automatically
|
|
233
|
+
|
|
234
|
+
# Access robot's memory
|
|
235
|
+
robot.memory[:user_id] = 123
|
|
236
|
+
robot.memory.data[:category] = "billing"
|
|
237
|
+
|
|
238
|
+
# Runtime memory injection
|
|
239
|
+
robot.run(message: "Help me", memory: { session_id: "abc123", tier: "premium" })
|
|
240
|
+
|
|
241
|
+
# Reset memory when needed
|
|
242
|
+
robot.reset_memory
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Networks pass context through SimpleFlow::Result:
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
# Create network with specialized robots
|
|
249
|
+
network = RobotLab.create_network(name: "support") do
|
|
250
|
+
task :classifier, classifier, depends_on: :none
|
|
251
|
+
task :billing, billing_robot, depends_on: :optional
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Run with context - available to all robots
|
|
255
|
+
result = network.run(
|
|
256
|
+
message: "I have a billing question",
|
|
257
|
+
customer_id: 456,
|
|
258
|
+
ticket_id: "TKT-123"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Access results from specific robots
|
|
262
|
+
classifier_result = result.context[:classifier]
|
|
263
|
+
billing_result = result.context[:billing]
|
|
264
|
+
|
|
265
|
+
# The final value is the last robot's output
|
|
266
|
+
puts result.value.last_text_content
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## MCP Integration
|
|
270
|
+
|
|
271
|
+
Connect to external tool servers via Model Context Protocol:
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
# Configure MCP server
|
|
275
|
+
filesystem_server = {
|
|
276
|
+
name: "filesystem",
|
|
277
|
+
transport: {
|
|
278
|
+
type: "stdio",
|
|
279
|
+
command: "mcp-server-filesystem",
|
|
280
|
+
args: ["/path/to/allowed/directory"]
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# Create robot with MCP server - tools are auto-discovered
|
|
285
|
+
robot = RobotLab.build(
|
|
286
|
+
name: "developer",
|
|
287
|
+
template: :developer,
|
|
288
|
+
mcp_servers: [filesystem_server]
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Robot can now use filesystem tools
|
|
292
|
+
result = robot.run(message: "List the files in the current directory")
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Streaming
|
|
296
|
+
|
|
297
|
+
Subscribe to real-time events during execution:
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
result = robot.run(message: "Tell me a story") do |event|
|
|
301
|
+
case event[:event]
|
|
302
|
+
when "text.delta"
|
|
303
|
+
print event[:data][:delta]
|
|
304
|
+
when "run.completed"
|
|
305
|
+
puts "\nDone!"
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Rails Integration
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
rails generate robot_lab:install
|
|
314
|
+
rails db:migrate
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
This creates:
|
|
318
|
+
- `config/initializers/robot_lab.rb` - Configuration
|
|
319
|
+
- `app/robots/` - Directory for your robots
|
|
320
|
+
- Database tables for conversation history
|
|
321
|
+
|
|
322
|
+
## Documentation
|
|
323
|
+
|
|
324
|
+
Full documentation is available at **[https://madbomber.github.io/robot_lab](https://madbomber.github.io/robot_lab)**
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
MIT License - Copyright (c) 2025 Dewayne VanHoozer
|
|
329
|
+
|
|
330
|
+
## Contributing
|
|
331
|
+
|
|
332
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/MadBomber/robot_lab.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
t.verbose = true
|
|
11
|
+
# Load test_helper before any tests run to ensure SimpleCov starts first
|
|
12
|
+
t.ruby_opts << "-rtest_helper"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
task default: :test
|
|
16
|
+
|
|
17
|
+
desc "Run tests with verbose output"
|
|
18
|
+
task :test_verbose do
|
|
19
|
+
ENV["TESTOPTS"] = "--verbose"
|
|
20
|
+
Rake::Task[:test].invoke
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "Run a single test file"
|
|
24
|
+
task :test_file, [:file] do |_t, args|
|
|
25
|
+
ruby "test/#{args[:file]}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "Run integration tests only"
|
|
29
|
+
Rake::TestTask.new(:integration) do |t|
|
|
30
|
+
t.libs << "test"
|
|
31
|
+
t.libs << "lib"
|
|
32
|
+
t.test_files = FileList["test/integration/**/*_test.rb"]
|
|
33
|
+
t.verbose = true
|
|
34
|
+
t.ruby_opts << "-rtest_helper"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "Check code style with RuboCop"
|
|
38
|
+
task :rubocop do
|
|
39
|
+
sh "bundle exec rubocop"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "Auto-correct RuboCop offenses"
|
|
43
|
+
task :rubocop_fix do
|
|
44
|
+
sh "bundle exec rubocop -a"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
namespace :examples do
|
|
48
|
+
desc "Run all examples"
|
|
49
|
+
task :all do
|
|
50
|
+
Dir.glob("examples/*.rb").sort.each do |example|
|
|
51
|
+
puts "\n#{'=' * 60}"
|
|
52
|
+
puts "Running: #{example}"
|
|
53
|
+
puts '=' * 60
|
|
54
|
+
ruby example
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc "Run a specific example by number (e.g., rake examples:run[1])"
|
|
59
|
+
task :run, [:num] do |_t, args|
|
|
60
|
+
example = Dir.glob("examples/#{args[:num].rjust(2, '0')}_*.rb").first
|
|
61
|
+
if example
|
|
62
|
+
ruby example
|
|
63
|
+
else
|
|
64
|
+
puts "Example #{args[:num]} not found"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Anthropic Adapter
|
|
2
|
+
|
|
3
|
+
Adapter for Claude models via Anthropic API.
|
|
4
|
+
|
|
5
|
+
## Class: `RobotLab::Adapters::Anthropic`
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Automatically used for Claude models
|
|
9
|
+
robot = RobotLab.build do
|
|
10
|
+
model "claude-sonnet-4"
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Supported Models
|
|
15
|
+
|
|
16
|
+
| Model | Description |
|
|
17
|
+
|-------|-------------|
|
|
18
|
+
| `claude-sonnet-4` | Latest Sonnet (recommended) |
|
|
19
|
+
| `claude-opus-4` | Most capable model |
|
|
20
|
+
| `claude-3-5-sonnet-latest` | Claude 3.5 Sonnet |
|
|
21
|
+
| `claude-3-5-haiku-latest` | Fast, efficient model |
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
### API Key
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export ANTHROPIC_API_KEY="sk-ant-api03-..."
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Options
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
RobotLab.configure do |config|
|
|
35
|
+
config.adapter_options = {
|
|
36
|
+
anthropic: {
|
|
37
|
+
base_url: "https://api.anthropic.com",
|
|
38
|
+
api_version: "2024-01-01",
|
|
39
|
+
timeout: 120,
|
|
40
|
+
max_tokens: 4096
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
### Streaming
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
result = robot.run(state: state) do |event|
|
|
52
|
+
case event
|
|
53
|
+
when :text_delta
|
|
54
|
+
print event.text
|
|
55
|
+
when :tool_call
|
|
56
|
+
puts "Calling: #{event.name}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Tool Use
|
|
62
|
+
|
|
63
|
+
Tools are automatically converted to Anthropic's format:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
robot = RobotLab.build do
|
|
67
|
+
model "claude-sonnet-4"
|
|
68
|
+
|
|
69
|
+
tool :search do
|
|
70
|
+
description "Search the database"
|
|
71
|
+
parameter :query, type: :string, required: true
|
|
72
|
+
handler { |query:, **_| Database.search(query) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Extended Thinking
|
|
78
|
+
|
|
79
|
+
For complex reasoning tasks:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
robot = RobotLab.build do
|
|
83
|
+
model "claude-sonnet-4"
|
|
84
|
+
# Extended thinking is automatically enabled for supported models
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Response Format
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
{
|
|
92
|
+
content: [TextMessage, ...],
|
|
93
|
+
tool_calls: [ToolCallMessage, ...],
|
|
94
|
+
usage: {
|
|
95
|
+
input_tokens: 150,
|
|
96
|
+
output_tokens: 250,
|
|
97
|
+
cache_creation_input_tokens: 0,
|
|
98
|
+
cache_read_input_tokens: 0
|
|
99
|
+
},
|
|
100
|
+
stop_reason: "end_turn"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Error Handling
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
begin
|
|
108
|
+
result = robot.run(state: state)
|
|
109
|
+
rescue RobotLab::Adapters::RateLimitError => e
|
|
110
|
+
sleep(e.retry_after)
|
|
111
|
+
retry
|
|
112
|
+
rescue RobotLab::Adapters::APIError => e
|
|
113
|
+
logger.error("Anthropic API error: #{e.message}")
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## See Also
|
|
118
|
+
|
|
119
|
+
- [Adapters Overview](index.md)
|
|
120
|
+
- [Streaming Guide](../../guides/streaming.md)
|
|
121
|
+
- [Anthropic API Documentation](https://docs.anthropic.com/)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Gemini Adapter
|
|
2
|
+
|
|
3
|
+
Adapter for Gemini models via Google AI API.
|
|
4
|
+
|
|
5
|
+
## Class: `RobotLab::Adapters::Gemini`
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Automatically used for Gemini models
|
|
9
|
+
robot = RobotLab.build do
|
|
10
|
+
model "gemini-1.5-pro"
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Supported Models
|
|
15
|
+
|
|
16
|
+
| Model | Description |
|
|
17
|
+
|-------|-------------|
|
|
18
|
+
| `gemini-1.5-pro` | Most capable Gemini |
|
|
19
|
+
| `gemini-1.5-flash` | Fast, efficient model |
|
|
20
|
+
| `gemini-1.5-flash-8b` | Lightweight model |
|
|
21
|
+
| `gemini-2.0-flash-exp` | Experimental next-gen |
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
### API Key
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export GOOGLE_AI_API_KEY="..."
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Options
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
RobotLab.configure do |config|
|
|
35
|
+
config.adapter_options = {
|
|
36
|
+
gemini: {
|
|
37
|
+
base_url: "https://generativelanguage.googleapis.com",
|
|
38
|
+
timeout: 120,
|
|
39
|
+
max_tokens: 8192
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Vertex AI
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
RobotLab.configure do |config|
|
|
49
|
+
config.adapter_options = {
|
|
50
|
+
gemini: {
|
|
51
|
+
base_url: "https://us-central1-aiplatform.googleapis.com",
|
|
52
|
+
project_id: "your-project",
|
|
53
|
+
location: "us-central1"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
### Streaming
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
result = robot.run(state: state) do |event|
|
|
65
|
+
case event
|
|
66
|
+
when :text_delta
|
|
67
|
+
print event.text
|
|
68
|
+
when :tool_call
|
|
69
|
+
puts "Calling: #{event.name}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Tool Use
|
|
75
|
+
|
|
76
|
+
Tools are automatically converted to Gemini's format:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
robot = RobotLab.build do
|
|
80
|
+
model "gemini-1.5-pro"
|
|
81
|
+
|
|
82
|
+
tool :search_products do
|
|
83
|
+
description "Search product catalog"
|
|
84
|
+
parameter :query, type: :string, required: true
|
|
85
|
+
parameter :category, type: :string
|
|
86
|
+
handler { |query:, category: nil, **_| Catalog.search(query, category) }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Long Context
|
|
92
|
+
|
|
93
|
+
Gemini supports very long contexts:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
robot = RobotLab.build do
|
|
97
|
+
model "gemini-1.5-pro"
|
|
98
|
+
# Supports up to 2M tokens context
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Response Format
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
{
|
|
106
|
+
content: [TextMessage, ...],
|
|
107
|
+
tool_calls: [ToolCallMessage, ...],
|
|
108
|
+
usage: {
|
|
109
|
+
input_tokens: 150,
|
|
110
|
+
output_tokens: 250
|
|
111
|
+
},
|
|
112
|
+
stop_reason: "STOP"
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Error Handling
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
begin
|
|
120
|
+
result = robot.run(state: state)
|
|
121
|
+
rescue RobotLab::Adapters::RateLimitError => e
|
|
122
|
+
sleep(e.retry_after || 60)
|
|
123
|
+
retry
|
|
124
|
+
rescue RobotLab::Adapters::APIError => e
|
|
125
|
+
logger.error("Gemini API error: #{e.message}")
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## See Also
|
|
130
|
+
|
|
131
|
+
- [Adapters Overview](index.md)
|
|
132
|
+
- [Streaming Guide](../../guides/streaming.md)
|
|
133
|
+
- [Google AI Documentation](https://ai.google.dev/docs)
|