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
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module Adapters
|
|
5
|
-
# Adapter for Google Gemini models
|
|
6
|
-
#
|
|
7
|
-
# Handles Gemini-specific API conventions:
|
|
8
|
-
# - Role mapping (assistant -> model)
|
|
9
|
-
# - Contents/parts array structure
|
|
10
|
-
# - functionCall/functionResponse format
|
|
11
|
-
#
|
|
12
|
-
class Gemini < Base
|
|
13
|
-
# Creates a new Gemini adapter instance.
|
|
14
|
-
def initialize
|
|
15
|
-
super(:gemini)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Format messages for Gemini API
|
|
19
|
-
#
|
|
20
|
-
# Gemini uses "model" role instead of "assistant" and structures
|
|
21
|
-
# content as parts arrays.
|
|
22
|
-
#
|
|
23
|
-
# @param messages [Array<Message>]
|
|
24
|
-
# @return [Array<Hash>]
|
|
25
|
-
#
|
|
26
|
-
def format_messages(messages)
|
|
27
|
-
# Gemini handles system messages differently - as system_instruction
|
|
28
|
-
conversation_messages(messages).map { |msg| format_single_message(msg) }
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Parse Gemini response into internal messages
|
|
32
|
-
#
|
|
33
|
-
# @param response [RubyLLM::Response]
|
|
34
|
-
# @return [Array<Message>]
|
|
35
|
-
#
|
|
36
|
-
def parse_response(response)
|
|
37
|
-
messages = []
|
|
38
|
-
|
|
39
|
-
# Handle text content
|
|
40
|
-
if response.content && !response.content.empty?
|
|
41
|
-
messages << TextMessage.new(
|
|
42
|
-
role: "assistant",
|
|
43
|
-
content: response.content,
|
|
44
|
-
stop_reason: response.tool_calls&.any? ? "tool" : "stop"
|
|
45
|
-
)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Handle function calls
|
|
49
|
-
if response.tool_calls&.any?
|
|
50
|
-
tool_messages = response.tool_calls.map do |id, tool_call|
|
|
51
|
-
ToolMessage.new(
|
|
52
|
-
id: id,
|
|
53
|
-
name: tool_call.name,
|
|
54
|
-
input: parse_tool_arguments(tool_call.arguments)
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
messages << ToolCallMessage.new(
|
|
59
|
-
role: "assistant",
|
|
60
|
-
tools: tool_messages,
|
|
61
|
-
stop_reason: "tool"
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
messages
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Format tools for Gemini function declarations
|
|
69
|
-
#
|
|
70
|
-
# Gemini doesn't support additionalProperties in schemas
|
|
71
|
-
#
|
|
72
|
-
# @param tools [Array<Tool>]
|
|
73
|
-
# @return [Array<Hash>]
|
|
74
|
-
#
|
|
75
|
-
def format_tools(tools)
|
|
76
|
-
tools.map do |tool|
|
|
77
|
-
schema = tool.to_json_schema
|
|
78
|
-
params = clean_schema_for_gemini(schema[:parameters] || { type: "object", properties: {} })
|
|
79
|
-
{
|
|
80
|
-
name: schema[:name],
|
|
81
|
-
description: schema[:description],
|
|
82
|
-
parameters: params
|
|
83
|
-
}
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Gemini tool choice format
|
|
88
|
-
#
|
|
89
|
-
# @param choice [String, Symbol]
|
|
90
|
-
# @return [Hash]
|
|
91
|
-
#
|
|
92
|
-
def format_tool_choice(choice)
|
|
93
|
-
case choice.to_s
|
|
94
|
-
when "auto" then { mode: "AUTO" }
|
|
95
|
-
when "any" then { mode: "ANY" }
|
|
96
|
-
when "none" then { mode: "NONE" }
|
|
97
|
-
else { mode: "ANY", allowed_function_names: [choice.to_s] }
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
private
|
|
102
|
-
|
|
103
|
-
def format_single_message(msg)
|
|
104
|
-
role = gemini_role(msg.role)
|
|
105
|
-
|
|
106
|
-
case msg
|
|
107
|
-
when TextMessage
|
|
108
|
-
{
|
|
109
|
-
role: role,
|
|
110
|
-
parts: [{ text: msg.content }]
|
|
111
|
-
}
|
|
112
|
-
when ToolCallMessage
|
|
113
|
-
{
|
|
114
|
-
role: "model",
|
|
115
|
-
parts: msg.tools.map do |tool|
|
|
116
|
-
{
|
|
117
|
-
functionCall: {
|
|
118
|
-
name: tool.name,
|
|
119
|
-
args: tool.input
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
end
|
|
123
|
-
}
|
|
124
|
-
when ToolResultMessage
|
|
125
|
-
{
|
|
126
|
-
role: "user",
|
|
127
|
-
parts: [
|
|
128
|
-
{
|
|
129
|
-
functionResponse: {
|
|
130
|
-
name: msg.tool.name,
|
|
131
|
-
response: format_tool_result_content(msg.content)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
]
|
|
135
|
-
}
|
|
136
|
-
else
|
|
137
|
-
{ role: role, parts: [{ text: msg.content.to_s }] }
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def gemini_role(role)
|
|
142
|
-
case role.to_s
|
|
143
|
-
when "assistant" then "model"
|
|
144
|
-
when "system" then "user" # Gemini handles system as system_instruction
|
|
145
|
-
else role.to_s
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def format_tool_result_content(content)
|
|
150
|
-
case content
|
|
151
|
-
when Hash
|
|
152
|
-
content
|
|
153
|
-
when String
|
|
154
|
-
{ result: content }
|
|
155
|
-
else
|
|
156
|
-
{ result: content.to_s }
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def parse_tool_arguments(arguments)
|
|
161
|
-
case arguments
|
|
162
|
-
when String
|
|
163
|
-
begin
|
|
164
|
-
JSON.parse(arguments, symbolize_names: true)
|
|
165
|
-
rescue JSON::ParserError
|
|
166
|
-
{ raw: arguments }
|
|
167
|
-
end
|
|
168
|
-
when Hash
|
|
169
|
-
arguments.transform_keys(&:to_sym)
|
|
170
|
-
else
|
|
171
|
-
arguments || {}
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Remove additionalProperties which Gemini doesn't support
|
|
176
|
-
def clean_schema_for_gemini(schema)
|
|
177
|
-
return schema unless schema.is_a?(Hash)
|
|
178
|
-
|
|
179
|
-
cleaned = schema.dup
|
|
180
|
-
cleaned.delete(:additionalProperties)
|
|
181
|
-
cleaned.delete("additionalProperties")
|
|
182
|
-
|
|
183
|
-
if cleaned[:properties]
|
|
184
|
-
cleaned[:properties] = cleaned[:properties].transform_values do |prop|
|
|
185
|
-
clean_schema_for_gemini(prop)
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
cleaned
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
end
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module Adapters
|
|
5
|
-
# Adapter for OpenAI GPT models
|
|
6
|
-
#
|
|
7
|
-
# Handles OpenAI-specific API conventions:
|
|
8
|
-
# - Function calling format
|
|
9
|
-
# - Strict mode for structured outputs
|
|
10
|
-
# - finish_reason to stop_reason mapping
|
|
11
|
-
#
|
|
12
|
-
class OpenAI < Base
|
|
13
|
-
# Creates a new OpenAI adapter instance.
|
|
14
|
-
def initialize
|
|
15
|
-
super(:openai)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Format messages for OpenAI API
|
|
19
|
-
#
|
|
20
|
-
# @param messages [Array<Message>]
|
|
21
|
-
# @return [Array<Hash>]
|
|
22
|
-
#
|
|
23
|
-
def format_messages(messages)
|
|
24
|
-
messages.map { |msg| format_single_message(msg) }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Parse OpenAI response into internal messages
|
|
28
|
-
#
|
|
29
|
-
# @param response [RubyLLM::Response]
|
|
30
|
-
# @return [Array<Message>]
|
|
31
|
-
#
|
|
32
|
-
def parse_response(response)
|
|
33
|
-
messages = []
|
|
34
|
-
|
|
35
|
-
# Handle text content
|
|
36
|
-
if response.content && !response.content.empty?
|
|
37
|
-
messages << TextMessage.new(
|
|
38
|
-
role: "assistant",
|
|
39
|
-
content: response.content,
|
|
40
|
-
stop_reason: response.tool_calls&.any? ? "tool" : "stop"
|
|
41
|
-
)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Handle tool calls
|
|
45
|
-
if response.tool_calls&.any?
|
|
46
|
-
tool_messages = response.tool_calls.map do |id, tool_call|
|
|
47
|
-
ToolMessage.new(
|
|
48
|
-
id: id,
|
|
49
|
-
name: tool_call.name,
|
|
50
|
-
input: parse_tool_arguments(tool_call.arguments)
|
|
51
|
-
)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
messages << ToolCallMessage.new(
|
|
55
|
-
role: "assistant",
|
|
56
|
-
tools: tool_messages,
|
|
57
|
-
stop_reason: "tool"
|
|
58
|
-
)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
messages
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Format tools for OpenAI function calling
|
|
65
|
-
#
|
|
66
|
-
# @param tools [Array<Tool>]
|
|
67
|
-
# @return [Array<Hash>]
|
|
68
|
-
#
|
|
69
|
-
def format_tools(tools)
|
|
70
|
-
tools.map do |tool|
|
|
71
|
-
schema = tool.to_json_schema
|
|
72
|
-
strict = tool.provider_params[:strict]
|
|
73
|
-
{
|
|
74
|
-
type: "function",
|
|
75
|
-
function: {
|
|
76
|
-
name: schema[:name],
|
|
77
|
-
description: schema[:description],
|
|
78
|
-
parameters: schema[:parameters] || { type: "object", properties: {} },
|
|
79
|
-
strict: strict.nil? ? true : strict
|
|
80
|
-
}.compact
|
|
81
|
-
}
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# OpenAI tool choice format
|
|
86
|
-
#
|
|
87
|
-
# @param choice [String, Symbol]
|
|
88
|
-
# @return [String, Hash]
|
|
89
|
-
#
|
|
90
|
-
def format_tool_choice(choice)
|
|
91
|
-
case choice.to_s
|
|
92
|
-
when "auto" then "auto"
|
|
93
|
-
when "any" then "required"
|
|
94
|
-
when "none" then "none"
|
|
95
|
-
else { type: "function", function: { name: choice.to_s } }
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
private
|
|
100
|
-
|
|
101
|
-
def format_single_message(msg)
|
|
102
|
-
case msg
|
|
103
|
-
when TextMessage
|
|
104
|
-
{ role: msg.role, content: msg.content }
|
|
105
|
-
when ToolCallMessage
|
|
106
|
-
{
|
|
107
|
-
role: "assistant",
|
|
108
|
-
content: nil,
|
|
109
|
-
tool_calls: msg.tools.map do |tool|
|
|
110
|
-
{
|
|
111
|
-
id: tool.id,
|
|
112
|
-
type: "function",
|
|
113
|
-
function: {
|
|
114
|
-
name: tool.name,
|
|
115
|
-
arguments: JSON.generate(tool.input)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
end
|
|
119
|
-
}
|
|
120
|
-
when ToolResultMessage
|
|
121
|
-
{
|
|
122
|
-
role: "tool",
|
|
123
|
-
tool_call_id: msg.tool.id,
|
|
124
|
-
content: format_tool_result_content(msg.content)
|
|
125
|
-
}
|
|
126
|
-
else
|
|
127
|
-
{ role: msg.role, content: msg.content.to_s }
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def format_tool_result_content(content)
|
|
132
|
-
case content
|
|
133
|
-
when Hash
|
|
134
|
-
JSON.generate(content)
|
|
135
|
-
when String
|
|
136
|
-
content
|
|
137
|
-
else
|
|
138
|
-
content.to_s
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def parse_tool_arguments(arguments)
|
|
143
|
-
case arguments
|
|
144
|
-
when String
|
|
145
|
-
# Handle OpenAI's backtick wrapping quirk
|
|
146
|
-
cleaned = arguments.gsub(/\A```(?:json)?\n?/, "").gsub(/\n?```\z/, "")
|
|
147
|
-
begin
|
|
148
|
-
JSON.parse(cleaned, symbolize_names: true)
|
|
149
|
-
rescue JSON::ParserError
|
|
150
|
-
{ raw: arguments }
|
|
151
|
-
end
|
|
152
|
-
when Hash
|
|
153
|
-
arguments.transform_keys(&:to_sym)
|
|
154
|
-
else
|
|
155
|
-
arguments || {}
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module Adapters
|
|
5
|
-
# Registry for looking up provider adapters
|
|
6
|
-
#
|
|
7
|
-
# Maps provider symbols to their adapter classes.
|
|
8
|
-
#
|
|
9
|
-
# @example
|
|
10
|
-
# adapter = Registry.for(:anthropic)
|
|
11
|
-
# adapter.format_messages(messages)
|
|
12
|
-
#
|
|
13
|
-
module Registry
|
|
14
|
-
# @return [Hash<Symbol, Class>] mapping of provider symbols to adapter classes
|
|
15
|
-
ADAPTERS = {
|
|
16
|
-
anthropic: Anthropic,
|
|
17
|
-
openai: OpenAI,
|
|
18
|
-
gemini: Gemini,
|
|
19
|
-
# Azure uses OpenAI adapter
|
|
20
|
-
azure_openai: OpenAI,
|
|
21
|
-
# Grok uses OpenAI adapter
|
|
22
|
-
grok: OpenAI,
|
|
23
|
-
# Ollama uses OpenAI adapter
|
|
24
|
-
ollama: OpenAI,
|
|
25
|
-
# OpenRouter uses OpenAI adapter
|
|
26
|
-
openrouter: OpenAI,
|
|
27
|
-
# Bedrock uses Anthropic adapter
|
|
28
|
-
bedrock: Anthropic,
|
|
29
|
-
# VertexAI uses Gemini adapter
|
|
30
|
-
vertexai: Gemini
|
|
31
|
-
}.freeze
|
|
32
|
-
|
|
33
|
-
class << self
|
|
34
|
-
# Get adapter for a provider
|
|
35
|
-
#
|
|
36
|
-
# @param provider [Symbol, String] Provider name
|
|
37
|
-
# @return [Base] Adapter instance
|
|
38
|
-
# @raise [ArgumentError] If provider not found
|
|
39
|
-
#
|
|
40
|
-
def for(provider)
|
|
41
|
-
provider_sym = provider.to_s.downcase.gsub("-", "_").to_sym
|
|
42
|
-
adapter_class = ADAPTERS[provider_sym]
|
|
43
|
-
|
|
44
|
-
unless adapter_class
|
|
45
|
-
raise ArgumentError, "Unknown provider: #{provider}. " \
|
|
46
|
-
"Available providers: #{available.join(', ')}"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
adapter_class.new
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# List available providers
|
|
53
|
-
#
|
|
54
|
-
# @return [Array<Symbol>]
|
|
55
|
-
#
|
|
56
|
-
def available
|
|
57
|
-
ADAPTERS.keys
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Check if provider is supported
|
|
61
|
-
#
|
|
62
|
-
# @param provider [Symbol, String]
|
|
63
|
-
# @return [Boolean]
|
|
64
|
-
#
|
|
65
|
-
def supports?(provider)
|
|
66
|
-
provider_sym = provider.to_s.downcase.gsub("-", "_").to_sym
|
|
67
|
-
ADAPTERS.key?(provider_sym)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Register a custom adapter
|
|
71
|
-
#
|
|
72
|
-
# @param provider [Symbol] Provider name
|
|
73
|
-
# @param adapter_class [Class] Adapter class
|
|
74
|
-
#
|
|
75
|
-
def register(provider, adapter_class)
|
|
76
|
-
ADAPTERS[provider.to_sym] = adapter_class
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
data/lib/robot_lab/errors.rb
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
# Error serialization utilities
|
|
5
|
-
#
|
|
6
|
-
# Provides methods to serialize Ruby exceptions into a format
|
|
7
|
-
# suitable for tool results and logging.
|
|
8
|
-
#
|
|
9
|
-
module Errors
|
|
10
|
-
class << self
|
|
11
|
-
# Serialize an exception to a hash
|
|
12
|
-
#
|
|
13
|
-
# @param error [Exception] The error to serialize
|
|
14
|
-
# @param include_backtrace [Boolean] Whether to include backtrace
|
|
15
|
-
# @return [Hash] Serialized error
|
|
16
|
-
#
|
|
17
|
-
def serialize(error, include_backtrace: false)
|
|
18
|
-
result = {
|
|
19
|
-
type: error.class.name,
|
|
20
|
-
message: error.message
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if include_backtrace && error.backtrace
|
|
24
|
-
result[:backtrace] = error.backtrace.first(10)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
if error.cause
|
|
28
|
-
result[:cause] = serialize(error.cause, include_backtrace: include_backtrace)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
result
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Deserialize an error hash back to an exception
|
|
35
|
-
#
|
|
36
|
-
# @param hash [Hash] Serialized error
|
|
37
|
-
# @return [StandardError]
|
|
38
|
-
#
|
|
39
|
-
def deserialize(hash)
|
|
40
|
-
hash = hash.transform_keys(&:to_sym)
|
|
41
|
-
klass = begin
|
|
42
|
-
Object.const_get(hash[:type])
|
|
43
|
-
rescue NameError
|
|
44
|
-
StandardError
|
|
45
|
-
end
|
|
46
|
-
klass.new(hash[:message])
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Format error for display
|
|
50
|
-
#
|
|
51
|
-
# @param error [Exception] The error
|
|
52
|
-
# @return [String]
|
|
53
|
-
#
|
|
54
|
-
def format(error)
|
|
55
|
-
"[#{error.class.name}] #{error.message}"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Wrap a block and return error hash on failure
|
|
59
|
-
#
|
|
60
|
-
# @yield Block to execute
|
|
61
|
-
# @return [Hash] { data: result } or { error: serialized_error }
|
|
62
|
-
#
|
|
63
|
-
def capture
|
|
64
|
-
{ data: yield }
|
|
65
|
-
rescue StandardError => e
|
|
66
|
-
{ error: serialize(e) }
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module History
|
|
5
|
-
# ActiveRecord-based history persistence adapter
|
|
6
|
-
#
|
|
7
|
-
# Provides thread and result storage using ActiveRecord models.
|
|
8
|
-
# Requires Rails or standalone ActiveRecord setup.
|
|
9
|
-
#
|
|
10
|
-
# @example
|
|
11
|
-
# adapter = ActiveRecordAdapter.new(
|
|
12
|
-
# thread_model: RobotLabThread,
|
|
13
|
-
# result_model: RobotLabResult
|
|
14
|
-
# )
|
|
15
|
-
#
|
|
16
|
-
# config = adapter.to_config
|
|
17
|
-
# network = RobotLab.create_network(history: config)
|
|
18
|
-
#
|
|
19
|
-
class ActiveRecordAdapter
|
|
20
|
-
# @!attribute [r] thread_model
|
|
21
|
-
# @return [Class] ActiveRecord model class for threads
|
|
22
|
-
# @!attribute [r] result_model
|
|
23
|
-
# @return [Class] ActiveRecord model class for results
|
|
24
|
-
attr_reader :thread_model, :result_model
|
|
25
|
-
|
|
26
|
-
# Initialize adapter with ActiveRecord models
|
|
27
|
-
#
|
|
28
|
-
# @param thread_model [Class] ActiveRecord model for threads
|
|
29
|
-
# @param result_model [Class] ActiveRecord model for results
|
|
30
|
-
#
|
|
31
|
-
def initialize(thread_model:, result_model:)
|
|
32
|
-
@thread_model = thread_model
|
|
33
|
-
@result_model = result_model
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Create a new thread
|
|
37
|
-
#
|
|
38
|
-
# @param state [State] Current state
|
|
39
|
-
# @param input [String, UserMessage] Initial input
|
|
40
|
-
# @return [Hash] Thread ID and metadata
|
|
41
|
-
#
|
|
42
|
-
def create_thread(state:, input:, **)
|
|
43
|
-
input_content = input.is_a?(UserMessage) ? input.content : input.to_s
|
|
44
|
-
input_metadata = input.is_a?(UserMessage) ? input.metadata : {}
|
|
45
|
-
|
|
46
|
-
thread = @thread_model.create!(
|
|
47
|
-
session_id: SecureRandom.uuid,
|
|
48
|
-
initial_input: input_content,
|
|
49
|
-
input_metadata: input_metadata,
|
|
50
|
-
state_data: state.data.to_h
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
{ session_id: thread.session_id, created_at: thread.created_at }
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Retrieve results for a thread
|
|
57
|
-
#
|
|
58
|
-
# @param session_id [String] Thread identifier
|
|
59
|
-
# @return [Array<RobotResult>] History of results
|
|
60
|
-
#
|
|
61
|
-
def get(session_id:, **)
|
|
62
|
-
@result_model
|
|
63
|
-
.where(session_id: session_id)
|
|
64
|
-
.order(:sequence_number, :created_at)
|
|
65
|
-
.map { |record| deserialize_result(record) }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Append user message to thread
|
|
69
|
-
#
|
|
70
|
-
# @param session_id [String] Thread identifier
|
|
71
|
-
# @param message [UserMessage] Message to append
|
|
72
|
-
#
|
|
73
|
-
def append_user_message(session_id:, message:, **)
|
|
74
|
-
@thread_model.where(session_id: session_id).update_all(
|
|
75
|
-
last_user_message: message.content,
|
|
76
|
-
last_user_message_at: Time.current
|
|
77
|
-
)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Append results to thread
|
|
81
|
-
#
|
|
82
|
-
# @param session_id [String] Thread identifier
|
|
83
|
-
# @param new_results [Array<RobotResult>] Results to append
|
|
84
|
-
#
|
|
85
|
-
def append_results(session_id:, new_results:, **)
|
|
86
|
-
base_sequence = @result_model.where(session_id: session_id).maximum(:sequence_number) || 0
|
|
87
|
-
|
|
88
|
-
new_results.each_with_index do |result, index|
|
|
89
|
-
@result_model.create!(
|
|
90
|
-
session_id: session_id,
|
|
91
|
-
robot_name: result.robot_name,
|
|
92
|
-
sequence_number: base_sequence + index + 1,
|
|
93
|
-
output_data: serialize_messages(result.output),
|
|
94
|
-
tool_calls_data: serialize_messages(result.tool_calls),
|
|
95
|
-
stop_reason: result.stop_reason,
|
|
96
|
-
checksum: result.checksum
|
|
97
|
-
)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Update thread timestamp
|
|
101
|
-
@thread_model.where(session_id: session_id).update_all(
|
|
102
|
-
updated_at: Time.current
|
|
103
|
-
)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Convert adapter to Config object
|
|
107
|
-
#
|
|
108
|
-
# @return [Config] History configuration
|
|
109
|
-
#
|
|
110
|
-
def to_config
|
|
111
|
-
Config.new(
|
|
112
|
-
create_thread: method(:create_thread),
|
|
113
|
-
get: method(:get),
|
|
114
|
-
append_user_message: method(:append_user_message),
|
|
115
|
-
append_results: method(:append_results)
|
|
116
|
-
)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
private
|
|
120
|
-
|
|
121
|
-
def serialize_messages(messages)
|
|
122
|
-
messages.map(&:to_h)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def deserialize_result(record)
|
|
126
|
-
output = deserialize_messages(record.output_data)
|
|
127
|
-
tool_calls = deserialize_messages(record.tool_calls_data)
|
|
128
|
-
|
|
129
|
-
RobotResult.new(
|
|
130
|
-
robot_name: record.robot_name,
|
|
131
|
-
output: output,
|
|
132
|
-
tool_calls: tool_calls,
|
|
133
|
-
stop_reason: record.stop_reason
|
|
134
|
-
)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def deserialize_messages(data)
|
|
138
|
-
return [] unless data
|
|
139
|
-
|
|
140
|
-
data.map do |msg_hash|
|
|
141
|
-
Message.from_hash(msg_hash.symbolize_keys)
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
end
|