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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# ActiveRecordAdapter
|
|
2
|
+
|
|
3
|
+
Rails ActiveRecord integration for conversation persistence.
|
|
4
|
+
|
|
5
|
+
## Class: `RobotLab::History::ActiveRecordAdapter`
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
adapter = History::ActiveRecordAdapter.new(
|
|
9
|
+
thread_model: ConversationThread,
|
|
10
|
+
result_model: ConversationResult
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
config = adapter.to_config
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Constructor
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
ActiveRecordAdapter.new(
|
|
20
|
+
thread_model:,
|
|
21
|
+
result_model:,
|
|
22
|
+
thread_factory: nil,
|
|
23
|
+
result_factory: nil
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Parameters:**
|
|
28
|
+
|
|
29
|
+
| Name | Type | Description |
|
|
30
|
+
|------|------|-------------|
|
|
31
|
+
| `thread_model` | `Class` | ActiveRecord model for threads |
|
|
32
|
+
| `result_model` | `Class` | ActiveRecord model for results |
|
|
33
|
+
| `thread_factory` | `Proc`, `nil` | Custom thread creation |
|
|
34
|
+
| `result_factory` | `Proc`, `nil` | Custom result creation |
|
|
35
|
+
|
|
36
|
+
## Methods
|
|
37
|
+
|
|
38
|
+
### to_config
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
config = adapter.to_config
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Convert to `History::Config` for use with networks.
|
|
45
|
+
|
|
46
|
+
## Model Requirements
|
|
47
|
+
|
|
48
|
+
### Thread Model
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# db/migrate/xxx_create_conversation_threads.rb
|
|
52
|
+
create_table :conversation_threads do |t|
|
|
53
|
+
t.string :external_id, null: false, index: { unique: true }
|
|
54
|
+
t.jsonb :metadata, default: {}
|
|
55
|
+
t.timestamps
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# app/models/conversation_thread.rb
|
|
59
|
+
class ConversationThread < ApplicationRecord
|
|
60
|
+
has_many :results, class_name: "ConversationResult",
|
|
61
|
+
foreign_key: :thread_id, dependent: :destroy
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Result Model
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# db/migrate/xxx_create_conversation_results.rb
|
|
69
|
+
create_table :conversation_results do |t|
|
|
70
|
+
t.references :thread, foreign_key: { to_table: :conversation_threads }
|
|
71
|
+
t.string :robot_name
|
|
72
|
+
t.jsonb :input, default: {}
|
|
73
|
+
t.jsonb :output, default: []
|
|
74
|
+
t.jsonb :tool_calls, default: []
|
|
75
|
+
t.jsonb :metadata, default: {}
|
|
76
|
+
t.integer :position
|
|
77
|
+
t.timestamps
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# app/models/conversation_result.rb
|
|
81
|
+
class ConversationResult < ApplicationRecord
|
|
82
|
+
belongs_to :thread, class_name: "ConversationThread"
|
|
83
|
+
|
|
84
|
+
def to_robot_result
|
|
85
|
+
RobotLab::RobotResult.from_hash(attributes)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Examples
|
|
91
|
+
|
|
92
|
+
### Basic Setup
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
adapter = History::ActiveRecordAdapter.new(
|
|
96
|
+
thread_model: ConversationThread,
|
|
97
|
+
result_model: ConversationResult
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
network = RobotLab.create_network do
|
|
101
|
+
name "chat"
|
|
102
|
+
history adapter.to_config
|
|
103
|
+
add_robot assistant
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### With Custom Factory
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
adapter = History::ActiveRecordAdapter.new(
|
|
111
|
+
thread_model: ConversationThread,
|
|
112
|
+
result_model: ConversationResult,
|
|
113
|
+
thread_factory: ->(state:, input:, **context) {
|
|
114
|
+
ConversationThread.create!(
|
|
115
|
+
external_id: SecureRandom.uuid,
|
|
116
|
+
user_id: context[:user_id],
|
|
117
|
+
title: input.truncate(100),
|
|
118
|
+
metadata: { source: context[:source] }
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### With User Scoping
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
class ScopedAdapter
|
|
128
|
+
def initialize(thread_model:, result_model:)
|
|
129
|
+
@thread_model = thread_model
|
|
130
|
+
@result_model = result_model
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def to_config
|
|
134
|
+
History::Config.new(
|
|
135
|
+
create_thread: method(:create_thread),
|
|
136
|
+
get: method(:get),
|
|
137
|
+
append_results: method(:append_results)
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def create_thread(state:, input:, user_id:, **)
|
|
144
|
+
@thread_model.create!(
|
|
145
|
+
external_id: SecureRandom.uuid,
|
|
146
|
+
user_id: user_id,
|
|
147
|
+
title: input.truncate(100)
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def get(thread_id:, user_id:, **)
|
|
152
|
+
thread = @thread_model.find_by(external_id: thread_id, user_id: user_id)
|
|
153
|
+
return [] unless thread
|
|
154
|
+
thread.results.order(:position).map(&:to_robot_result)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def append_results(thread_id:, new_results:, user_id:, **)
|
|
158
|
+
thread = @thread_model.find_by!(external_id: thread_id, user_id: user_id)
|
|
159
|
+
position = thread.results.maximum(:position) || 0
|
|
160
|
+
|
|
161
|
+
@result_model.transaction do
|
|
162
|
+
new_results.each_with_index do |result, i|
|
|
163
|
+
thread.results.create!(
|
|
164
|
+
robot_name: result.robot_name,
|
|
165
|
+
input: result.input.to_h,
|
|
166
|
+
output: result.output.map(&:to_h),
|
|
167
|
+
tool_calls: result.tool_calls.map(&:to_h),
|
|
168
|
+
position: position + i + 1
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Rails Generator
|
|
177
|
+
|
|
178
|
+
Use the Rails generator to create models:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
rails generate robot_lab:history
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This creates:
|
|
185
|
+
|
|
186
|
+
- `ConversationThread` model
|
|
187
|
+
- `ConversationResult` model
|
|
188
|
+
- Database migrations
|
|
189
|
+
- Initializer configuration
|
|
190
|
+
|
|
191
|
+
## See Also
|
|
192
|
+
|
|
193
|
+
- [History Overview](index.md)
|
|
194
|
+
- [Config](config.md)
|
|
195
|
+
- [Rails Integration Guide](../../guides/rails-integration.md)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# History::Config
|
|
2
|
+
|
|
3
|
+
Configuration for conversation persistence.
|
|
4
|
+
|
|
5
|
+
## Class: `RobotLab::History::Config`
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
config = History::Config.new(
|
|
9
|
+
create_thread: create_proc,
|
|
10
|
+
get: get_proc,
|
|
11
|
+
append_results: append_proc
|
|
12
|
+
)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Constructor
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
Config.new(
|
|
19
|
+
create_thread:,
|
|
20
|
+
get:,
|
|
21
|
+
append_results:
|
|
22
|
+
)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Parameters:**
|
|
26
|
+
|
|
27
|
+
| Name | Type | Description |
|
|
28
|
+
|------|------|-------------|
|
|
29
|
+
| `create_thread` | `Proc` | Creates a new thread |
|
|
30
|
+
| `get` | `Proc` | Retrieves thread history |
|
|
31
|
+
| `append_results` | `Proc` | Appends results to thread |
|
|
32
|
+
|
|
33
|
+
## Callbacks
|
|
34
|
+
|
|
35
|
+
### create_thread
|
|
36
|
+
|
|
37
|
+
Called when a new conversation starts without a thread_id.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
create_thread: ->(state:, input:, **context) {
|
|
41
|
+
# Create and return thread info
|
|
42
|
+
{ id: SecureRandom.uuid }
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Arguments:**
|
|
47
|
+
|
|
48
|
+
| Name | Type | Description |
|
|
49
|
+
|------|------|-------------|
|
|
50
|
+
| `state` | `State` | Current state |
|
|
51
|
+
| `input` | `String` | User input |
|
|
52
|
+
| `**context` | `Hash` | Additional context |
|
|
53
|
+
|
|
54
|
+
**Returns:** Hash with `:id` key.
|
|
55
|
+
|
|
56
|
+
### get
|
|
57
|
+
|
|
58
|
+
Called to retrieve existing conversation history.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
get: ->(thread_id:, **context) {
|
|
62
|
+
# Return array of previous results
|
|
63
|
+
Thread.find(thread_id).results
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Arguments:**
|
|
68
|
+
|
|
69
|
+
| Name | Type | Description |
|
|
70
|
+
|------|------|-------------|
|
|
71
|
+
| `thread_id` | `String` | Thread identifier |
|
|
72
|
+
| `**context` | `Hash` | Additional context |
|
|
73
|
+
|
|
74
|
+
**Returns:** Array of `RobotResult` or hashes.
|
|
75
|
+
|
|
76
|
+
### append_results
|
|
77
|
+
|
|
78
|
+
Called after each network run to persist new results.
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
append_results: ->(thread_id:, new_results:, **context) {
|
|
82
|
+
# Persist the new results
|
|
83
|
+
thread = Thread.find(thread_id)
|
|
84
|
+
new_results.each { |r| thread.results.create(r.to_h) }
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Arguments:**
|
|
89
|
+
|
|
90
|
+
| Name | Type | Description |
|
|
91
|
+
|------|------|-------------|
|
|
92
|
+
| `thread_id` | `String` | Thread identifier |
|
|
93
|
+
| `new_results` | `Array<RobotResult>` | Results to append |
|
|
94
|
+
| `**context` | `Hash` | Additional context |
|
|
95
|
+
|
|
96
|
+
## Attributes
|
|
97
|
+
|
|
98
|
+
### create_thread
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
config.create_thread # => Proc
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### get
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
config.get # => Proc
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### append_results
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
config.append_results # => Proc
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Examples
|
|
117
|
+
|
|
118
|
+
### Basic Config
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
STORE = {}
|
|
122
|
+
|
|
123
|
+
config = History::Config.new(
|
|
124
|
+
create_thread: ->(state:, **) {
|
|
125
|
+
id = SecureRandom.uuid
|
|
126
|
+
STORE[id] = { results: [] }
|
|
127
|
+
{ id: id }
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
get: ->(thread_id:, **) {
|
|
131
|
+
STORE.dig(thread_id, :results) || []
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
append_results: ->(thread_id:, new_results:, **) {
|
|
135
|
+
STORE[thread_id][:results].concat(new_results.map(&:to_h))
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### With Context
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
config = History::Config.new(
|
|
144
|
+
create_thread: ->(state:, user_id:, **) {
|
|
145
|
+
Thread.create(user_id: user_id, started_at: Time.current)
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
get: ->(thread_id:, user_id:, **) {
|
|
149
|
+
Thread.where(id: thread_id, user_id: user_id).first&.results || []
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
append_results: ->(thread_id:, new_results:, user_id:, **) {
|
|
153
|
+
thread = Thread.find_by(id: thread_id, user_id: user_id)
|
|
154
|
+
return unless thread
|
|
155
|
+
new_results.each { |r| thread.results.create(r.to_h) }
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Pass context when running
|
|
160
|
+
network.run(state: state, user_id: current_user.id)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### With Validation
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
config = History::Config.new(
|
|
167
|
+
create_thread: ->(state:, **) {
|
|
168
|
+
raise "Invalid state" unless state.data[:user_id]
|
|
169
|
+
Thread.create(user_id: state.data[:user_id])
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
get: ->(thread_id:, **) {
|
|
173
|
+
thread = Thread.find_by(id: thread_id)
|
|
174
|
+
raise "Thread not found" unless thread
|
|
175
|
+
thread.results
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
append_results: ->(thread_id:, new_results:, **) {
|
|
179
|
+
thread = Thread.find(thread_id)
|
|
180
|
+
Thread.transaction do
|
|
181
|
+
new_results.each { |r| thread.results.create!(r.to_h) }
|
|
182
|
+
end
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## See Also
|
|
188
|
+
|
|
189
|
+
- [History Overview](index.md)
|
|
190
|
+
- [ThreadManager](thread-manager.md)
|
|
191
|
+
- [ActiveRecordAdapter](active-record-adapter.md)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# History
|
|
2
|
+
|
|
3
|
+
Conversation persistence and thread management.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The history system enables persistent conversations by storing and retrieving conversation threads and results.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
network = RobotLab.create_network do
|
|
11
|
+
name "persistent_chat"
|
|
12
|
+
|
|
13
|
+
history History::Config.new(
|
|
14
|
+
create_thread: ->(state:, **) { Thread.create(id: SecureRandom.uuid) },
|
|
15
|
+
get: ->(thread_id:, **) { Thread.find(thread_id).results },
|
|
16
|
+
append_results: ->(thread_id:, new_results:, **) {
|
|
17
|
+
Thread.find(thread_id).results.concat(new_results)
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
add_robot assistant
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Components
|
|
26
|
+
|
|
27
|
+
| Component | Description |
|
|
28
|
+
|-----------|-------------|
|
|
29
|
+
| [Config](config.md) | History configuration |
|
|
30
|
+
| [ThreadManager](thread-manager.md) | Thread lifecycle management |
|
|
31
|
+
| [ActiveRecordAdapter](active-record-adapter.md) | Rails integration |
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### Basic Configuration
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
history = History::Config.new(
|
|
39
|
+
create_thread: ->(state:, input:, **) {
|
|
40
|
+
{ id: SecureRandom.uuid }
|
|
41
|
+
},
|
|
42
|
+
get: ->(thread_id:, **) {
|
|
43
|
+
STORE[thread_id] || []
|
|
44
|
+
},
|
|
45
|
+
append_results: ->(thread_id:, new_results:, **) {
|
|
46
|
+
STORE[thread_id] ||= []
|
|
47
|
+
STORE[thread_id].concat(new_results)
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### With ActiveRecord
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
history = History::ActiveRecordAdapter.new(
|
|
56
|
+
thread_model: ConversationThread,
|
|
57
|
+
result_model: ConversationResult
|
|
58
|
+
).to_config
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Callbacks
|
|
62
|
+
|
|
63
|
+
| Callback | Purpose |
|
|
64
|
+
|----------|---------|
|
|
65
|
+
| `create_thread` | Create new conversation thread |
|
|
66
|
+
| `get` | Retrieve existing thread history |
|
|
67
|
+
| `append_results` | Add results to thread |
|
|
68
|
+
|
|
69
|
+
## Thread Lifecycle
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
sequenceDiagram
|
|
73
|
+
participant U as User
|
|
74
|
+
participant N as Network
|
|
75
|
+
participant H as History
|
|
76
|
+
|
|
77
|
+
U->>N: Run with new message
|
|
78
|
+
N->>H: get(thread_id)
|
|
79
|
+
H-->>N: Previous results
|
|
80
|
+
N->>N: Execute robots
|
|
81
|
+
N->>H: append_results(new_results)
|
|
82
|
+
H-->>N: Saved
|
|
83
|
+
N-->>U: Result with thread_id
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Examples
|
|
87
|
+
|
|
88
|
+
### In-Memory Store
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
THREADS = {}
|
|
92
|
+
|
|
93
|
+
history = History::Config.new(
|
|
94
|
+
create_thread: ->(state:, **) {
|
|
95
|
+
id = SecureRandom.uuid
|
|
96
|
+
THREADS[id] = []
|
|
97
|
+
{ id: id }
|
|
98
|
+
},
|
|
99
|
+
get: ->(thread_id:, **) {
|
|
100
|
+
THREADS[thread_id] || []
|
|
101
|
+
},
|
|
102
|
+
append_results: ->(thread_id:, new_results:, **) {
|
|
103
|
+
THREADS[thread_id].concat(new_results)
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Redis Store
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
history = History::Config.new(
|
|
112
|
+
create_thread: ->(state:, **) {
|
|
113
|
+
id = SecureRandom.uuid
|
|
114
|
+
Redis.current.set("thread:#{id}", [].to_json)
|
|
115
|
+
{ id: id }
|
|
116
|
+
},
|
|
117
|
+
get: ->(thread_id:, **) {
|
|
118
|
+
data = Redis.current.get("thread:#{thread_id}")
|
|
119
|
+
data ? JSON.parse(data) : []
|
|
120
|
+
},
|
|
121
|
+
append_results: ->(thread_id:, new_results:, **) {
|
|
122
|
+
existing = JSON.parse(Redis.current.get("thread:#{thread_id}") || "[]")
|
|
123
|
+
existing.concat(new_results.map(&:to_h))
|
|
124
|
+
Redis.current.set("thread:#{thread_id}", existing.to_json)
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## See Also
|
|
130
|
+
|
|
131
|
+
- [History Guide](../../guides/history.md)
|
|
132
|
+
- [State](../core/state.md)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# ThreadManager
|
|
2
|
+
|
|
3
|
+
Manages conversation thread lifecycle.
|
|
4
|
+
|
|
5
|
+
## Class: `RobotLab::History::ThreadManager`
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
manager = History::ThreadManager.new(config: history_config)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Constructor
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
ThreadManager.new(config:)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Parameters:**
|
|
18
|
+
|
|
19
|
+
| Name | Type | Description |
|
|
20
|
+
|------|------|-------------|
|
|
21
|
+
| `config` | `Config` | History configuration |
|
|
22
|
+
|
|
23
|
+
## Methods
|
|
24
|
+
|
|
25
|
+
### create_thread
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
thread_info = manager.create_thread(state: state, input: input, **context)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Create a new conversation thread.
|
|
32
|
+
|
|
33
|
+
**Returns:** Hash with `:id` and optional metadata.
|
|
34
|
+
|
|
35
|
+
### get_history
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
results = manager.get_history(thread_id: id, **context)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Retrieve conversation history.
|
|
42
|
+
|
|
43
|
+
**Returns:** Array of `RobotResult`.
|
|
44
|
+
|
|
45
|
+
### append_results
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
manager.append_results(thread_id: id, new_results: results, **context)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Add results to a thread.
|
|
52
|
+
|
|
53
|
+
### ensure_thread
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
thread_id = manager.ensure_thread(state: state, input: input, **context)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Create thread if state doesn't have one, or return existing.
|
|
60
|
+
|
|
61
|
+
### load_history
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
state = manager.load_history(state: state, **context)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Load history into state if thread_id exists.
|
|
68
|
+
|
|
69
|
+
## Examples
|
|
70
|
+
|
|
71
|
+
### Basic Usage
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
config = History::Config.new(...)
|
|
75
|
+
manager = History::ThreadManager.new(config: config)
|
|
76
|
+
|
|
77
|
+
# Start new conversation
|
|
78
|
+
state = RobotLab.create_state(message: "Hello")
|
|
79
|
+
thread_id = manager.ensure_thread(state: state, input: "Hello")
|
|
80
|
+
state.thread_id = thread_id
|
|
81
|
+
|
|
82
|
+
# Run and save
|
|
83
|
+
result = network.run(state: state)
|
|
84
|
+
manager.append_results(thread_id: thread_id, new_results: result.new_results)
|
|
85
|
+
|
|
86
|
+
# Continue conversation
|
|
87
|
+
state2 = RobotLab.create_state(message: "Follow up")
|
|
88
|
+
state2.thread_id = thread_id
|
|
89
|
+
state2 = manager.load_history(state: state2)
|
|
90
|
+
# state2 now has previous results
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### In Network
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# ThreadManager is used internally by Network
|
|
97
|
+
network = RobotLab.create_network do
|
|
98
|
+
history config
|
|
99
|
+
add_robot assistant
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# First message - thread created automatically
|
|
103
|
+
result1 = network.run(state: state1)
|
|
104
|
+
thread_id = result1.state.thread_id
|
|
105
|
+
|
|
106
|
+
# Continue - history loaded automatically
|
|
107
|
+
state2 = RobotLab.create_state(
|
|
108
|
+
message: UserMessage.new("Continue", thread_id: thread_id)
|
|
109
|
+
)
|
|
110
|
+
result2 = network.run(state: state2)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Custom Thread Data
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
manager = History::ThreadManager.new(
|
|
117
|
+
config: History::Config.new(
|
|
118
|
+
create_thread: ->(state:, input:, metadata:, **) {
|
|
119
|
+
Thread.create(
|
|
120
|
+
title: input.truncate(50),
|
|
121
|
+
metadata: metadata,
|
|
122
|
+
created_at: Time.current
|
|
123
|
+
)
|
|
124
|
+
},
|
|
125
|
+
get: ->(thread_id:, **) { Thread.find(thread_id).results },
|
|
126
|
+
append_results: ->(thread_id:, new_results:, **) {
|
|
127
|
+
Thread.find(thread_id).results.concat(new_results)
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Pass metadata when creating thread
|
|
133
|
+
thread = manager.create_thread(
|
|
134
|
+
state: state,
|
|
135
|
+
input: "Help with billing",
|
|
136
|
+
metadata: { source: "web", priority: "high" }
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## See Also
|
|
141
|
+
|
|
142
|
+
- [History Overview](index.md)
|
|
143
|
+
- [Config](config.md)
|
|
144
|
+
- [ActiveRecordAdapter](active-record-adapter.md)
|