robot_lab 0.0.4 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +64 -6
- data/Rakefile +2 -1
- data/docs/api/core/index.md +41 -46
- data/docs/api/core/memory.md +200 -154
- data/docs/api/core/network.md +13 -3
- data/docs/api/core/robot.md +38 -26
- data/docs/api/core/state.md +55 -73
- data/docs/api/index.md +7 -28
- data/docs/api/messages/index.md +35 -20
- data/docs/api/messages/text-message.md +67 -21
- data/docs/api/messages/tool-call-message.md +80 -41
- data/docs/api/messages/tool-result-message.md +119 -50
- data/docs/api/messages/user-message.md +48 -24
- data/docs/architecture/core-concepts.md +10 -15
- data/docs/concepts.md +5 -7
- data/docs/examples/index.md +2 -2
- data/docs/getting-started/configuration.md +80 -0
- data/docs/guides/building-robots.md +10 -9
- data/docs/guides/creating-networks.md +49 -0
- data/docs/guides/index.md +0 -5
- data/docs/guides/rails-integration.md +244 -162
- data/docs/guides/streaming.md +118 -138
- data/docs/index.md +0 -8
- data/examples/03_network.rb +10 -7
- data/examples/08_llm_config.rb +40 -11
- data/examples/09_chaining.rb +45 -6
- data/examples/11_network_introspection.rb +30 -7
- data/examples/12_message_bus.rb +1 -1
- data/examples/14_rusty_circuit/heckler.rb +14 -8
- data/examples/14_rusty_circuit/open_mic.rb +5 -3
- data/examples/14_rusty_circuit/scout.rb +14 -31
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +1 -1
- data/examples/16_writers_room/display.rb +158 -0
- data/examples/16_writers_room/output/.gitignore +2 -0
- data/examples/16_writers_room/output/opus_001.md +263 -0
- data/examples/16_writers_room/output/opus_001_notes.log +470 -0
- data/examples/16_writers_room/prompts/writer.md +37 -0
- data/examples/16_writers_room/room.rb +150 -0
- data/examples/16_writers_room/tools.rb +162 -0
- data/examples/16_writers_room/writer.rb +121 -0
- data/examples/16_writers_room/writers_room.rb +162 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
- data/lib/robot_lab/memory.rb +8 -32
- data/lib/robot_lab/network.rb +13 -20
- data/lib/robot_lab/robot/bus_messaging.rb +239 -0
- data/lib/robot_lab/robot/mcp_management.rb +88 -0
- data/lib/robot_lab/robot/template_rendering.rb +130 -0
- data/lib/robot_lab/robot.rb +56 -420
- data/lib/robot_lab/run_config.rb +184 -0
- data/lib/robot_lab/state_proxy.rb +2 -12
- data/lib/robot_lab/task.rb +8 -1
- data/lib/robot_lab/utils.rb +39 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +29 -8
- data/mkdocs.yml +0 -11
- metadata +15 -20
- data/docs/api/adapters/anthropic.md +0 -121
- data/docs/api/adapters/gemini.md +0 -133
- data/docs/api/adapters/index.md +0 -104
- data/docs/api/adapters/openai.md +0 -134
- data/docs/api/history/active-record-adapter.md +0 -275
- data/docs/api/history/config.md +0 -284
- data/docs/api/history/index.md +0 -128
- data/docs/api/history/thread-manager.md +0 -194
- data/docs/guides/history.md +0 -359
- data/lib/robot_lab/adapters/anthropic.rb +0 -163
- data/lib/robot_lab/adapters/base.rb +0 -85
- data/lib/robot_lab/adapters/gemini.rb +0 -193
- data/lib/robot_lab/adapters/openai.rb +0 -160
- data/lib/robot_lab/adapters/registry.rb +0 -81
- data/lib/robot_lab/errors.rb +0 -70
- data/lib/robot_lab/history/active_record_adapter.rb +0 -146
- data/lib/robot_lab/history/config.rb +0 -115
- data/lib/robot_lab/history/thread_manager.rb +0 -93
- data/lib/robot_lab/robotic_model.rb +0 -324
data/docs/guides/history.md
DELETED
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
# Conversation History
|
|
2
|
-
|
|
3
|
-
Persist and restore conversation threads across sessions.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
History allows you to:
|
|
8
|
-
|
|
9
|
-
- Save conversation results to a database
|
|
10
|
-
- Restore previous conversations
|
|
11
|
-
- Continue multi-turn interactions
|
|
12
|
-
- Maintain context across sessions
|
|
13
|
-
|
|
14
|
-
## Configuration
|
|
15
|
-
|
|
16
|
-
### History Config
|
|
17
|
-
|
|
18
|
-
Configure history with callbacks:
|
|
19
|
-
|
|
20
|
-
```ruby
|
|
21
|
-
history_config = RobotLab::History::Config.new(
|
|
22
|
-
create_thread: ->(state:, input:, **) {
|
|
23
|
-
# Create a new thread, return thread_id
|
|
24
|
-
{ thread_id: SecureRandom.uuid }
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
get: ->(thread_id:, **) {
|
|
28
|
-
# Retrieve history for thread
|
|
29
|
-
# Return Array<RobotResult>
|
|
30
|
-
[]
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
append_user_message: ->(thread_id:, message:, **) {
|
|
34
|
-
# Optional: Store user message
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
append_results: ->(thread_id:, new_results:, **) {
|
|
38
|
-
# Store new results
|
|
39
|
-
}
|
|
40
|
-
)
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Apply to Network
|
|
44
|
-
|
|
45
|
-
```ruby
|
|
46
|
-
network = RobotLab.create_network do
|
|
47
|
-
name "persistent_chat"
|
|
48
|
-
history history_config
|
|
49
|
-
end
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Callback Reference
|
|
53
|
-
|
|
54
|
-
### create_thread
|
|
55
|
-
|
|
56
|
-
Called when starting a new conversation:
|
|
57
|
-
|
|
58
|
-
```ruby
|
|
59
|
-
create_thread: ->(state:, input:, **kwargs) {
|
|
60
|
-
# state - Current State object
|
|
61
|
-
# input - UserMessage or string
|
|
62
|
-
# kwargs - Additional context
|
|
63
|
-
|
|
64
|
-
thread = Thread.create!(
|
|
65
|
-
initial_input: input.to_s,
|
|
66
|
-
user_id: state.data[:user_id]
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
{ thread_id: thread.id.to_s } # Must return hash with :thread_id
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### get
|
|
74
|
-
|
|
75
|
-
Called to retrieve existing history:
|
|
76
|
-
|
|
77
|
-
```ruby
|
|
78
|
-
get: ->(thread_id:, **kwargs) {
|
|
79
|
-
# thread_id - The thread identifier
|
|
80
|
-
# kwargs - Additional context
|
|
81
|
-
|
|
82
|
-
Result.where(thread_id: thread_id)
|
|
83
|
-
.order(:created_at)
|
|
84
|
-
.map { |r| deserialize_result(r) }
|
|
85
|
-
|
|
86
|
-
# Must return Array<RobotResult>
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### append_user_message (Optional)
|
|
91
|
-
|
|
92
|
-
Called when a user message is added:
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
append_user_message: ->(thread_id:, message:, **kwargs) {
|
|
96
|
-
# thread_id - The thread identifier
|
|
97
|
-
# message - UserMessage object
|
|
98
|
-
|
|
99
|
-
Message.create!(
|
|
100
|
-
thread_id: thread_id,
|
|
101
|
-
content: message.content,
|
|
102
|
-
metadata: message.metadata
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### append_results
|
|
108
|
-
|
|
109
|
-
Called after robots finish:
|
|
110
|
-
|
|
111
|
-
```ruby
|
|
112
|
-
append_results: ->(thread_id:, new_results:, **kwargs) {
|
|
113
|
-
# thread_id - The thread identifier
|
|
114
|
-
# new_results - Array<RobotResult>
|
|
115
|
-
|
|
116
|
-
new_results.each do |result|
|
|
117
|
-
Result.create!(
|
|
118
|
-
thread_id: thread_id,
|
|
119
|
-
robot_name: result.robot_name,
|
|
120
|
-
output_data: serialize_output(result.output),
|
|
121
|
-
stop_reason: result.stop_reason
|
|
122
|
-
)
|
|
123
|
-
end
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## ActiveRecord Adapter
|
|
128
|
-
|
|
129
|
-
RobotLab includes a built-in ActiveRecord adapter:
|
|
130
|
-
|
|
131
|
-
```ruby
|
|
132
|
-
adapter = RobotLab::History::ActiveRecordAdapter.new(
|
|
133
|
-
thread_model: RobotLabThread,
|
|
134
|
-
result_model: RobotLabResult
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
network = RobotLab.create_network do
|
|
138
|
-
history adapter.to_config
|
|
139
|
-
end
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### Required Models
|
|
143
|
-
|
|
144
|
-
```ruby title="app/models/robot_lab_thread.rb"
|
|
145
|
-
class RobotLabThread < ApplicationRecord
|
|
146
|
-
has_many :results, class_name: "RobotLabResult", foreign_key: :thread_id
|
|
147
|
-
|
|
148
|
-
# Required columns:
|
|
149
|
-
# - thread_id: string
|
|
150
|
-
# - initial_input: text
|
|
151
|
-
# - input_metadata: jsonb
|
|
152
|
-
# - state_data: jsonb
|
|
153
|
-
# - last_user_message: text
|
|
154
|
-
# - last_user_message_at: datetime
|
|
155
|
-
end
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
```ruby title="app/models/robot_lab_result.rb"
|
|
159
|
-
class RobotLabResult < ApplicationRecord
|
|
160
|
-
belongs_to :thread, class_name: "RobotLabThread", foreign_key: :thread_id
|
|
161
|
-
|
|
162
|
-
# Required columns:
|
|
163
|
-
# - thread_id: string
|
|
164
|
-
# - robot_name: string
|
|
165
|
-
# - sequence_number: integer
|
|
166
|
-
# - output_data: jsonb
|
|
167
|
-
# - tool_calls_data: jsonb
|
|
168
|
-
# - stop_reason: string
|
|
169
|
-
# - checksum: string
|
|
170
|
-
end
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## Using Thread IDs
|
|
174
|
-
|
|
175
|
-
### Start New Thread
|
|
176
|
-
|
|
177
|
-
```ruby
|
|
178
|
-
state = RobotLab.create_state(message: "Hello!")
|
|
179
|
-
result = network.run(state: state)
|
|
180
|
-
|
|
181
|
-
# Thread ID is assigned automatically
|
|
182
|
-
thread_id = state.thread_id
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### Continue Existing Thread
|
|
186
|
-
|
|
187
|
-
```ruby
|
|
188
|
-
# Option 1: Via UserMessage
|
|
189
|
-
message = RobotLab::UserMessage.new(
|
|
190
|
-
"Continue our conversation",
|
|
191
|
-
thread_id: existing_thread_id
|
|
192
|
-
)
|
|
193
|
-
state = RobotLab.create_state(message: message)
|
|
194
|
-
|
|
195
|
-
# Option 2: Direct assignment
|
|
196
|
-
state = RobotLab.create_state(message: "Continue")
|
|
197
|
-
state.thread_id = existing_thread_id
|
|
198
|
-
|
|
199
|
-
# History is automatically loaded
|
|
200
|
-
result = network.run(state: state)
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## ThreadManager
|
|
204
|
-
|
|
205
|
-
For programmatic control:
|
|
206
|
-
|
|
207
|
-
```ruby
|
|
208
|
-
manager = RobotLab::History::ThreadManager.new(history_config)
|
|
209
|
-
|
|
210
|
-
# Create thread
|
|
211
|
-
thread_id = manager.create_thread(state: state, input: message)
|
|
212
|
-
|
|
213
|
-
# Load history
|
|
214
|
-
results = manager.get_history(thread_id)
|
|
215
|
-
|
|
216
|
-
# Save state
|
|
217
|
-
manager.save_state(thread_id: thread_id, state: state, since_index: 5)
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
## Serialization
|
|
221
|
-
|
|
222
|
-
### RobotResult
|
|
223
|
-
|
|
224
|
-
Results are serialized via `export`:
|
|
225
|
-
|
|
226
|
-
```ruby
|
|
227
|
-
result.export
|
|
228
|
-
# => {
|
|
229
|
-
# robot_name: "assistant",
|
|
230
|
-
# output: [...],
|
|
231
|
-
# tool_calls: [...],
|
|
232
|
-
# stop_reason: "stop",
|
|
233
|
-
# id: "...",
|
|
234
|
-
# created_at: "..."
|
|
235
|
-
# }
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### Messages
|
|
239
|
-
|
|
240
|
-
Messages serialize to hashes:
|
|
241
|
-
|
|
242
|
-
```ruby
|
|
243
|
-
message.to_h
|
|
244
|
-
# => {
|
|
245
|
-
# type: "text",
|
|
246
|
-
# role: "assistant",
|
|
247
|
-
# content: "Hello!",
|
|
248
|
-
# stop_reason: "stop"
|
|
249
|
-
# }
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### Restore from hash
|
|
253
|
-
|
|
254
|
-
```ruby
|
|
255
|
-
RobotLab::Message.from_hash(hash)
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
## Patterns
|
|
259
|
-
|
|
260
|
-
### Redis-Based History
|
|
261
|
-
|
|
262
|
-
```ruby
|
|
263
|
-
history_config = History::Config.new(
|
|
264
|
-
create_thread: ->(state:, input:, **) {
|
|
265
|
-
thread_id = SecureRandom.uuid
|
|
266
|
-
Redis.current.hset("threads", thread_id, input.to_s)
|
|
267
|
-
{ thread_id: thread_id }
|
|
268
|
-
},
|
|
269
|
-
|
|
270
|
-
get: ->(thread_id:, **) {
|
|
271
|
-
data = Redis.current.lrange("results:#{thread_id}", 0, -1)
|
|
272
|
-
data.map { |json| deserialize_result(JSON.parse(json)) }
|
|
273
|
-
},
|
|
274
|
-
|
|
275
|
-
append_results: ->(thread_id:, new_results:, **) {
|
|
276
|
-
new_results.each do |result|
|
|
277
|
-
Redis.current.rpush("results:#{thread_id}", result.export.to_json)
|
|
278
|
-
end
|
|
279
|
-
}
|
|
280
|
-
)
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
### Custom Storage
|
|
284
|
-
|
|
285
|
-
```ruby
|
|
286
|
-
class CustomHistoryAdapter
|
|
287
|
-
def initialize(storage)
|
|
288
|
-
@storage = storage
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
def to_config
|
|
292
|
-
History::Config.new(
|
|
293
|
-
create_thread: method(:create_thread),
|
|
294
|
-
get: method(:get),
|
|
295
|
-
append_results: method(:append_results)
|
|
296
|
-
)
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
private
|
|
300
|
-
|
|
301
|
-
def create_thread(state:, input:, **)
|
|
302
|
-
id = @storage.create_conversation(input: input.to_s)
|
|
303
|
-
{ thread_id: id }
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def get(thread_id:, **)
|
|
307
|
-
@storage.fetch_results(thread_id)
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
def append_results(thread_id:, new_results:, **)
|
|
311
|
-
@storage.store_results(thread_id, new_results)
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
## Best Practices
|
|
317
|
-
|
|
318
|
-
### 1. Handle Missing Threads
|
|
319
|
-
|
|
320
|
-
```ruby
|
|
321
|
-
get: ->(thread_id:, **) {
|
|
322
|
-
thread = Thread.find_by(thread_id: thread_id)
|
|
323
|
-
return [] unless thread
|
|
324
|
-
|
|
325
|
-
thread.results.order(:created_at).map(&:to_robot_result)
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### 2. Index for Performance
|
|
330
|
-
|
|
331
|
-
```sql
|
|
332
|
-
CREATE INDEX idx_results_thread_id ON robot_lab_results(thread_id);
|
|
333
|
-
CREATE INDEX idx_results_created_at ON robot_lab_results(created_at);
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
### 3. Clean Up Old Threads
|
|
337
|
-
|
|
338
|
-
```ruby
|
|
339
|
-
# Periodic cleanup job
|
|
340
|
-
Thread.where("updated_at < ?", 30.days.ago).destroy_all
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
### 4. Limit History Size
|
|
344
|
-
|
|
345
|
-
```ruby
|
|
346
|
-
get: ->(thread_id:, **) {
|
|
347
|
-
Result.where(thread_id: thread_id)
|
|
348
|
-
.order(created_at: :desc)
|
|
349
|
-
.limit(50) # Last 50 exchanges
|
|
350
|
-
.reverse
|
|
351
|
-
.map(&:to_robot_result)
|
|
352
|
-
}
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
## Next Steps
|
|
356
|
-
|
|
357
|
-
- [Memory System](memory.md) - In-memory data sharing
|
|
358
|
-
- [State Management](../architecture/state-management.md) - State details
|
|
359
|
-
- [API Reference: History](../api/history/index.md) - Complete API
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module Adapters
|
|
5
|
-
# Adapter for Anthropic Claude models
|
|
6
|
-
#
|
|
7
|
-
# Handles Anthropic-specific API conventions:
|
|
8
|
-
# - System message as top-level parameter (not in messages array)
|
|
9
|
-
# - Tool use/result format differences
|
|
10
|
-
# - Content block structure
|
|
11
|
-
#
|
|
12
|
-
class Anthropic < Base
|
|
13
|
-
# Creates a new Anthropic adapter instance.
|
|
14
|
-
def initialize
|
|
15
|
-
super(:anthropic)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Format messages for Anthropic API
|
|
19
|
-
#
|
|
20
|
-
# Anthropic requires system message at top level, not in messages array.
|
|
21
|
-
# Also handles tool_use and tool_result message formats.
|
|
22
|
-
#
|
|
23
|
-
# @param messages [Array<Message>]
|
|
24
|
-
# @return [Array<Hash>]
|
|
25
|
-
#
|
|
26
|
-
def format_messages(messages)
|
|
27
|
-
conversation_messages(messages).map do |msg|
|
|
28
|
-
format_single_message(msg)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Parse Anthropic response into internal messages
|
|
33
|
-
#
|
|
34
|
-
# @param response [RubyLLM::Response] ruby_llm response object
|
|
35
|
-
# @return [Array<Message>]
|
|
36
|
-
#
|
|
37
|
-
def parse_response(response)
|
|
38
|
-
messages = []
|
|
39
|
-
|
|
40
|
-
# Handle text content
|
|
41
|
-
if response.content && !response.content.empty?
|
|
42
|
-
messages << TextMessage.new(
|
|
43
|
-
role: "assistant",
|
|
44
|
-
content: response.content,
|
|
45
|
-
stop_reason: response.tool_calls&.any? ? "tool" : "stop"
|
|
46
|
-
)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Handle tool calls
|
|
50
|
-
if response.tool_calls&.any?
|
|
51
|
-
tool_messages = response.tool_calls.map do |id, tool_call|
|
|
52
|
-
ToolMessage.new(
|
|
53
|
-
id: id,
|
|
54
|
-
name: tool_call.name,
|
|
55
|
-
input: parse_tool_arguments(tool_call.arguments)
|
|
56
|
-
)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
messages << ToolCallMessage.new(
|
|
60
|
-
role: "assistant",
|
|
61
|
-
tools: tool_messages,
|
|
62
|
-
stop_reason: "tool"
|
|
63
|
-
)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
messages
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Format tools for Anthropic
|
|
70
|
-
#
|
|
71
|
-
# @param tools [Array<Tool>]
|
|
72
|
-
# @return [Array<Hash>]
|
|
73
|
-
#
|
|
74
|
-
def format_tools(tools)
|
|
75
|
-
tools.map do |tool|
|
|
76
|
-
schema = tool.to_json_schema
|
|
77
|
-
{
|
|
78
|
-
name: schema[:name],
|
|
79
|
-
description: schema[:description],
|
|
80
|
-
input_schema: schema[:parameters] || { type: "object", properties: {} }
|
|
81
|
-
}
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Anthropic tool choice format
|
|
86
|
-
#
|
|
87
|
-
# @param choice [String, Symbol]
|
|
88
|
-
# @return [Hash]
|
|
89
|
-
#
|
|
90
|
-
def format_tool_choice(choice)
|
|
91
|
-
case choice.to_s
|
|
92
|
-
when "auto" then { type: "auto" }
|
|
93
|
-
when "any" then { type: "any" }
|
|
94
|
-
else { type: "tool", name: choice.to_s }
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
private
|
|
99
|
-
|
|
100
|
-
def format_single_message(msg)
|
|
101
|
-
case msg
|
|
102
|
-
when TextMessage
|
|
103
|
-
{ role: msg.role, content: msg.content }
|
|
104
|
-
when ToolCallMessage
|
|
105
|
-
{
|
|
106
|
-
role: "assistant",
|
|
107
|
-
content: msg.tools.map do |tool|
|
|
108
|
-
{
|
|
109
|
-
type: "tool_use",
|
|
110
|
-
id: tool.id,
|
|
111
|
-
name: tool.name,
|
|
112
|
-
input: tool.input
|
|
113
|
-
}
|
|
114
|
-
end
|
|
115
|
-
}
|
|
116
|
-
when ToolResultMessage
|
|
117
|
-
{
|
|
118
|
-
role: "user",
|
|
119
|
-
content: [
|
|
120
|
-
{
|
|
121
|
-
type: "tool_result",
|
|
122
|
-
tool_use_id: msg.tool.id,
|
|
123
|
-
content: format_tool_result_content(msg.content)
|
|
124
|
-
}
|
|
125
|
-
]
|
|
126
|
-
}
|
|
127
|
-
else
|
|
128
|
-
{ role: msg.role, content: msg.content.to_s }
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def format_tool_result_content(content)
|
|
133
|
-
case content
|
|
134
|
-
when Hash
|
|
135
|
-
if content[:error]
|
|
136
|
-
JSON.generate(content)
|
|
137
|
-
elsif content[:data]
|
|
138
|
-
content[:data].is_a?(String) ? content[:data] : JSON.generate(content[:data])
|
|
139
|
-
else
|
|
140
|
-
JSON.generate(content)
|
|
141
|
-
end
|
|
142
|
-
else
|
|
143
|
-
content.to_s
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def parse_tool_arguments(arguments)
|
|
148
|
-
case arguments
|
|
149
|
-
when String
|
|
150
|
-
begin
|
|
151
|
-
JSON.parse(arguments, symbolize_names: true)
|
|
152
|
-
rescue JSON::ParserError
|
|
153
|
-
{ raw: arguments }
|
|
154
|
-
end
|
|
155
|
-
when Hash
|
|
156
|
-
arguments.transform_keys(&:to_sym)
|
|
157
|
-
else
|
|
158
|
-
arguments || {}
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module Adapters
|
|
5
|
-
# Base adapter interface for LLM providers
|
|
6
|
-
#
|
|
7
|
-
# Adapters handle provider-specific message formatting and response parsing.
|
|
8
|
-
# Each provider (Anthropic, OpenAI, Gemini) has different API conventions
|
|
9
|
-
# that the adapter normalizes.
|
|
10
|
-
#
|
|
11
|
-
# @abstract Subclass and implement {#format_messages} and {#parse_response}
|
|
12
|
-
#
|
|
13
|
-
class Base
|
|
14
|
-
# @!attribute [r] provider
|
|
15
|
-
# @return [Symbol] the provider name
|
|
16
|
-
attr_reader :provider
|
|
17
|
-
|
|
18
|
-
# Creates a new adapter instance.
|
|
19
|
-
#
|
|
20
|
-
# @param provider [Symbol] the provider name
|
|
21
|
-
def initialize(provider)
|
|
22
|
-
@provider = provider
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Format internal messages for the provider's API
|
|
26
|
-
#
|
|
27
|
-
# @param messages [Array<Message>] Internal message format
|
|
28
|
-
# @return [Array<Hash>] Provider-specific message format
|
|
29
|
-
#
|
|
30
|
-
def format_messages(messages)
|
|
31
|
-
raise NotImplementedError, "#{self.class}#format_messages must be implemented"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Parse provider response into internal message format
|
|
35
|
-
#
|
|
36
|
-
# @param response [Object] Provider-specific response
|
|
37
|
-
# @return [Array<Message>] Internal message format
|
|
38
|
-
#
|
|
39
|
-
def parse_response(response)
|
|
40
|
-
raise NotImplementedError, "#{self.class}#parse_response must be implemented"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Format tools for the provider's function calling API
|
|
44
|
-
#
|
|
45
|
-
# @param tools [Array<Tool>] Internal tool definitions
|
|
46
|
-
# @return [Array<Hash>] Provider-specific tool format
|
|
47
|
-
#
|
|
48
|
-
def format_tools(tools)
|
|
49
|
-
tools.map(&:to_json_schema)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Format tool choice for the provider
|
|
53
|
-
#
|
|
54
|
-
# @param choice [String, Symbol] "auto", "any", or specific tool name
|
|
55
|
-
# @return [Object] Provider-specific tool choice
|
|
56
|
-
#
|
|
57
|
-
def format_tool_choice(choice)
|
|
58
|
-
case choice.to_s
|
|
59
|
-
when "auto" then "auto"
|
|
60
|
-
when "any" then "required"
|
|
61
|
-
else { type: "function", function: { name: choice.to_s } }
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Extract system message from messages array
|
|
66
|
-
#
|
|
67
|
-
# @param messages [Array<Message>]
|
|
68
|
-
# @return [String, nil]
|
|
69
|
-
#
|
|
70
|
-
def extract_system_message(messages)
|
|
71
|
-
system_msg = messages.find(&:system?)
|
|
72
|
-
system_msg&.content
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Filter out system messages
|
|
76
|
-
#
|
|
77
|
-
# @param messages [Array<Message>]
|
|
78
|
-
# @return [Array<Message>]
|
|
79
|
-
#
|
|
80
|
-
def conversation_messages(messages)
|
|
81
|
-
messages.reject(&:system?)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|