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,70 @@
|
|
|
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
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module History
|
|
5
|
+
# Configuration for conversation history persistence
|
|
6
|
+
#
|
|
7
|
+
# Defines callbacks for creating threads, retrieving history,
|
|
8
|
+
# and appending messages/results.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# config = History::Config.new(
|
|
12
|
+
# create_thread: ->(state:, input:, **) {
|
|
13
|
+
# { session_id: SecureRandom.uuid }
|
|
14
|
+
# },
|
|
15
|
+
# get: ->(session_id:, **) {
|
|
16
|
+
# database.find_results(session_id)
|
|
17
|
+
# },
|
|
18
|
+
# append_results: ->(session_id:, new_results:, **) {
|
|
19
|
+
# database.insert_results(session_id, new_results)
|
|
20
|
+
# }
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
class Config
|
|
24
|
+
# @!attribute [rw] create_thread
|
|
25
|
+
# @return [Proc, nil] callback to create a new conversation thread
|
|
26
|
+
# @!attribute [rw] get
|
|
27
|
+
# @return [Proc, nil] callback to retrieve history for a thread
|
|
28
|
+
# @!attribute [rw] append_user_message
|
|
29
|
+
# @return [Proc, nil] callback to append user messages
|
|
30
|
+
# @!attribute [rw] append_results
|
|
31
|
+
# @return [Proc, nil] callback to append robot results
|
|
32
|
+
attr_accessor :create_thread, :get, :append_user_message, :append_results
|
|
33
|
+
|
|
34
|
+
# Initialize history configuration
|
|
35
|
+
#
|
|
36
|
+
# @param create_thread [Proc] Callback to create a new thread
|
|
37
|
+
# @param get [Proc] Callback to retrieve history for a thread
|
|
38
|
+
# @param append_user_message [Proc] Callback to append user messages
|
|
39
|
+
# @param append_results [Proc] Callback to append robot results
|
|
40
|
+
#
|
|
41
|
+
def initialize(create_thread: nil, get: nil, append_user_message: nil, append_results: nil)
|
|
42
|
+
@create_thread = create_thread
|
|
43
|
+
@get = get
|
|
44
|
+
@append_user_message = append_user_message
|
|
45
|
+
@append_results = append_results
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if history persistence is configured
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
#
|
|
52
|
+
def configured?
|
|
53
|
+
@create_thread && @get
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Create a new conversation thread
|
|
57
|
+
#
|
|
58
|
+
# @param state [State] Current state
|
|
59
|
+
# @param input [String, UserMessage] Initial input
|
|
60
|
+
# @param kwargs [Hash] Additional arguments
|
|
61
|
+
# @return [Hash] Must include :session_id
|
|
62
|
+
#
|
|
63
|
+
def create_thread!(state:, input:, **kwargs)
|
|
64
|
+
raise HistoryError, "create_thread callback not configured" unless @create_thread
|
|
65
|
+
|
|
66
|
+
result = @create_thread.call(state: state, input: input, **kwargs)
|
|
67
|
+
|
|
68
|
+
unless result.is_a?(Hash) && result[:session_id]
|
|
69
|
+
raise HistoryError, "create_thread must return a hash with :session_id"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Retrieve history for a thread
|
|
76
|
+
#
|
|
77
|
+
# @param session_id [String] Thread identifier
|
|
78
|
+
# @param kwargs [Hash] Additional arguments
|
|
79
|
+
# @return [Array<RobotResult>] History of results
|
|
80
|
+
#
|
|
81
|
+
def get!(session_id:, **kwargs)
|
|
82
|
+
raise HistoryError, "get callback not configured" unless @get
|
|
83
|
+
|
|
84
|
+
@get.call(session_id: session_id, **kwargs)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Append a user message to the thread
|
|
88
|
+
#
|
|
89
|
+
# @param session_id [String] Thread identifier
|
|
90
|
+
# @param message [UserMessage] Message to append
|
|
91
|
+
# @param kwargs [Hash] Additional arguments
|
|
92
|
+
#
|
|
93
|
+
def append_user_message!(session_id:, message:, **kwargs)
|
|
94
|
+
return unless @append_user_message
|
|
95
|
+
|
|
96
|
+
@append_user_message.call(session_id: session_id, message: message, **kwargs)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Append robot results to the thread
|
|
100
|
+
#
|
|
101
|
+
# @param session_id [String] Thread identifier
|
|
102
|
+
# @param new_results [Array<RobotResult>] Results to append
|
|
103
|
+
# @param kwargs [Hash] Additional arguments
|
|
104
|
+
#
|
|
105
|
+
def append_results!(session_id:, new_results:, **kwargs)
|
|
106
|
+
return unless @append_results
|
|
107
|
+
|
|
108
|
+
@append_results.call(session_id: session_id, new_results: new_results, **kwargs)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Error raised when history operations fail
|
|
113
|
+
class HistoryError < RobotLab::Error; end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module History
|
|
5
|
+
# Manages conversation thread lifecycle
|
|
6
|
+
#
|
|
7
|
+
# Handles thread creation, history retrieval, and result persistence
|
|
8
|
+
# using the configured history adapter.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# manager = ThreadManager.new(config)
|
|
12
|
+
# session_id = manager.create_thread(state: state, input: "Hello")
|
|
13
|
+
# history = manager.get_history(session_id)
|
|
14
|
+
#
|
|
15
|
+
class ThreadManager
|
|
16
|
+
# @!attribute [r] config
|
|
17
|
+
# @return [Config] the history configuration
|
|
18
|
+
attr_reader :config
|
|
19
|
+
|
|
20
|
+
# Initialize thread manager
|
|
21
|
+
#
|
|
22
|
+
# @param config [Config] History configuration
|
|
23
|
+
#
|
|
24
|
+
def initialize(config)
|
|
25
|
+
@config = config
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create a new conversation thread
|
|
29
|
+
#
|
|
30
|
+
# @param state [State] Current state
|
|
31
|
+
# @param input [String, UserMessage] Initial input
|
|
32
|
+
# @return [String] Thread ID
|
|
33
|
+
#
|
|
34
|
+
def create_thread(state:, input:)
|
|
35
|
+
result = @config.create_thread!(state: state, input: input)
|
|
36
|
+
result[:session_id]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get history for a thread
|
|
40
|
+
#
|
|
41
|
+
# @param session_id [String] Thread identifier
|
|
42
|
+
# @return [Array<RobotResult>] History of results
|
|
43
|
+
#
|
|
44
|
+
def get_history(session_id)
|
|
45
|
+
@config.get!(session_id: session_id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Append user message to thread
|
|
49
|
+
#
|
|
50
|
+
# @param session_id [String] Thread identifier
|
|
51
|
+
# @param message [UserMessage] Message to append
|
|
52
|
+
#
|
|
53
|
+
def append_user_message(session_id:, message:)
|
|
54
|
+
@config.append_user_message!(session_id: session_id, message: message)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Append results to thread
|
|
58
|
+
#
|
|
59
|
+
# @param session_id [String] Thread identifier
|
|
60
|
+
# @param results [Array<RobotResult>] Results to append
|
|
61
|
+
#
|
|
62
|
+
def append_results(session_id:, results:)
|
|
63
|
+
@config.append_results!(session_id: session_id, new_results: results)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Load state from thread history
|
|
67
|
+
#
|
|
68
|
+
# @param session_id [String] Thread identifier
|
|
69
|
+
# @param state [State, Memory] State/Memory to populate
|
|
70
|
+
# @return [State, Memory] State/Memory with loaded history
|
|
71
|
+
#
|
|
72
|
+
def load_state(session_id:, state:)
|
|
73
|
+
results = get_history(session_id)
|
|
74
|
+
|
|
75
|
+
state.session_id = session_id
|
|
76
|
+
results.each { |r| state.append_result(r) }
|
|
77
|
+
|
|
78
|
+
state
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Save state results to thread
|
|
82
|
+
#
|
|
83
|
+
# @param session_id [String] Thread identifier
|
|
84
|
+
# @param state [State] State with results to save
|
|
85
|
+
# @param since_index [Integer] Save results from this index
|
|
86
|
+
#
|
|
87
|
+
def save_state(session_id:, state:, since_index: 0)
|
|
88
|
+
new_results = state.results[since_index..]
|
|
89
|
+
append_results(session_id: session_id, results: new_results) if new_results.any?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module MCP
|
|
5
|
+
# MCP client for communicating with Model Context Protocol servers
|
|
6
|
+
#
|
|
7
|
+
# Uses actionmcp gem for MCP protocol implementation.
|
|
8
|
+
# Supports multiple transport types: StdIO, SSE, WebSocket, HTTP.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# client = Client.new(name: "neon", transport: { type: "ws", url: "ws://..." })
|
|
12
|
+
# client.connect
|
|
13
|
+
# tools = client.list_tools
|
|
14
|
+
# result = client.call_tool("createBranch", { project_id: "abc" })
|
|
15
|
+
#
|
|
16
|
+
class Client
|
|
17
|
+
# @!attribute [r] server
|
|
18
|
+
# @return [Server] the MCP server configuration
|
|
19
|
+
# @!attribute [r] connected
|
|
20
|
+
# @return [Boolean] whether currently connected
|
|
21
|
+
attr_reader :server, :connected
|
|
22
|
+
|
|
23
|
+
# Creates a new MCP Client instance.
|
|
24
|
+
#
|
|
25
|
+
# @param server_or_config [Server, Hash] the server or configuration hash
|
|
26
|
+
# @raise [ArgumentError] if config is invalid
|
|
27
|
+
def initialize(server_or_config)
|
|
28
|
+
@server = case server_or_config
|
|
29
|
+
when Server
|
|
30
|
+
server_or_config
|
|
31
|
+
when Hash
|
|
32
|
+
Server.new(**server_or_config.transform_keys(&:to_sym))
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, "Invalid server config"
|
|
35
|
+
end
|
|
36
|
+
@connected = false
|
|
37
|
+
@transport = nil
|
|
38
|
+
@request_id = 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Connect to the MCP server
|
|
42
|
+
#
|
|
43
|
+
# @return [self]
|
|
44
|
+
#
|
|
45
|
+
def connect
|
|
46
|
+
return self if @connected
|
|
47
|
+
|
|
48
|
+
@transport = create_transport
|
|
49
|
+
@transport.connect if @transport.respond_to?(:connect)
|
|
50
|
+
@connected = true
|
|
51
|
+
|
|
52
|
+
self
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
RobotLab.configuration.logger.warn("MCP connection failed for #{@server.name}: #{e.message}")
|
|
55
|
+
@connected = false
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Disconnect from the server
|
|
60
|
+
#
|
|
61
|
+
# @return [self]
|
|
62
|
+
#
|
|
63
|
+
def disconnect
|
|
64
|
+
return self unless @connected
|
|
65
|
+
|
|
66
|
+
@transport.close if @transport.respond_to?(:close)
|
|
67
|
+
@connected = false
|
|
68
|
+
@transport = nil
|
|
69
|
+
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# List available tools from the server
|
|
74
|
+
#
|
|
75
|
+
# @return [Array<Hash>] Tool definitions
|
|
76
|
+
#
|
|
77
|
+
def list_tools
|
|
78
|
+
ensure_connected!
|
|
79
|
+
response = request(method: "tools/list")
|
|
80
|
+
response[:tools] || []
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Call a tool on the server
|
|
84
|
+
#
|
|
85
|
+
# @param name [String] Tool name
|
|
86
|
+
# @param arguments [Hash] Tool arguments
|
|
87
|
+
# @return [Object] Tool result
|
|
88
|
+
#
|
|
89
|
+
def call_tool(name, arguments = {})
|
|
90
|
+
ensure_connected!
|
|
91
|
+
response = request(
|
|
92
|
+
method: "tools/call",
|
|
93
|
+
params: { name: name, arguments: arguments }
|
|
94
|
+
)
|
|
95
|
+
response[:content] || response
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# List available resources
|
|
99
|
+
#
|
|
100
|
+
# @return [Array<Hash>]
|
|
101
|
+
#
|
|
102
|
+
def list_resources
|
|
103
|
+
ensure_connected!
|
|
104
|
+
response = request(method: "resources/list")
|
|
105
|
+
response[:resources] || []
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Read a resource
|
|
109
|
+
#
|
|
110
|
+
# @param uri [String] Resource URI
|
|
111
|
+
# @return [Object]
|
|
112
|
+
#
|
|
113
|
+
def read_resource(uri)
|
|
114
|
+
ensure_connected!
|
|
115
|
+
response = request(method: "resources/read", params: { uri: uri })
|
|
116
|
+
response[:contents] || response
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# List available prompts
|
|
120
|
+
#
|
|
121
|
+
# @return [Array<Hash>]
|
|
122
|
+
#
|
|
123
|
+
def list_prompts
|
|
124
|
+
ensure_connected!
|
|
125
|
+
response = request(method: "prompts/list")
|
|
126
|
+
response[:prompts] || []
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Get a prompt
|
|
130
|
+
#
|
|
131
|
+
# @param name [String] Prompt name
|
|
132
|
+
# @param arguments [Hash] Prompt arguments
|
|
133
|
+
# @return [Hash]
|
|
134
|
+
#
|
|
135
|
+
def get_prompt(name, arguments = {})
|
|
136
|
+
ensure_connected!
|
|
137
|
+
response = request(method: "prompts/get", params: { name: name, arguments: arguments })
|
|
138
|
+
response
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Checks if the client is connected to the server.
|
|
142
|
+
#
|
|
143
|
+
# @return [Boolean]
|
|
144
|
+
def connected?
|
|
145
|
+
@connected
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Converts the client to a hash representation.
|
|
149
|
+
#
|
|
150
|
+
# @return [Hash]
|
|
151
|
+
def to_h
|
|
152
|
+
{
|
|
153
|
+
server: @server.to_h,
|
|
154
|
+
connected: @connected
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def ensure_connected!
|
|
161
|
+
raise MCPError, "Not connected to MCP server: #{@server.name}" unless @connected
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def create_transport
|
|
165
|
+
case @server.transport_type
|
|
166
|
+
when "stdio"
|
|
167
|
+
Transports::Stdio.new(@server.transport)
|
|
168
|
+
when "ws", "websocket"
|
|
169
|
+
Transports::WebSocket.new(@server.transport)
|
|
170
|
+
when "sse"
|
|
171
|
+
Transports::SSE.new(@server.transport)
|
|
172
|
+
when "streamable-http", "http"
|
|
173
|
+
Transports::StreamableHTTP.new(@server.transport)
|
|
174
|
+
else
|
|
175
|
+
raise MCPError, "Unsupported transport type: #{@server.transport_type}"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def request(method:, params: nil)
|
|
180
|
+
@request_id += 1
|
|
181
|
+
|
|
182
|
+
message = {
|
|
183
|
+
jsonrpc: "2.0",
|
|
184
|
+
id: @request_id,
|
|
185
|
+
method: method
|
|
186
|
+
}
|
|
187
|
+
message[:params] = params if params
|
|
188
|
+
|
|
189
|
+
response = @transport.send_request(message)
|
|
190
|
+
parse_response(response)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def parse_response(response)
|
|
194
|
+
return response[:result] if response.is_a?(Hash) && response[:result]
|
|
195
|
+
|
|
196
|
+
case response
|
|
197
|
+
when String
|
|
198
|
+
parsed = JSON.parse(response, symbolize_names: true)
|
|
199
|
+
parsed[:result] || parsed
|
|
200
|
+
when Hash
|
|
201
|
+
response[:result] || response
|
|
202
|
+
else
|
|
203
|
+
response
|
|
204
|
+
end
|
|
205
|
+
rescue JSON::ParserError
|
|
206
|
+
{ raw: response }
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|