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,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Represents a change to a Memory key
|
|
5
|
+
#
|
|
6
|
+
# MemoryChange is passed to subscription callbacks when a key's value changes.
|
|
7
|
+
# It provides context about what changed, who changed it, and when.
|
|
8
|
+
#
|
|
9
|
+
# @example Subscription callback
|
|
10
|
+
# memory.subscribe(:sentiment) do |change|
|
|
11
|
+
# puts "Key #{change.key} changed from #{change.previous} to #{change.value}"
|
|
12
|
+
# puts "Written by: #{change.writer} at #{change.timestamp}"
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @note This class is designed to be compatible with SmartMessage::Base.
|
|
16
|
+
# When smart_message is added as a dependency, this class can inherit
|
|
17
|
+
# from SmartMessage::Base for distributed pub/sub support.
|
|
18
|
+
#
|
|
19
|
+
class MemoryChange
|
|
20
|
+
# @!attribute [r] key
|
|
21
|
+
# @return [Symbol] the memory key that changed
|
|
22
|
+
# @!attribute [r] value
|
|
23
|
+
# @return [Object] the new value
|
|
24
|
+
# @!attribute [r] previous
|
|
25
|
+
# @return [Object, nil] the previous value (nil if key was created)
|
|
26
|
+
# @!attribute [r] writer
|
|
27
|
+
# @return [String, nil] name of the robot that wrote the value
|
|
28
|
+
# @!attribute [r] network_name
|
|
29
|
+
# @return [String, nil] name of the network
|
|
30
|
+
# @!attribute [r] timestamp
|
|
31
|
+
# @return [Time] when the change occurred
|
|
32
|
+
# @!attribute [r] correlation_id
|
|
33
|
+
# @return [String, nil] optional correlation ID for tracing
|
|
34
|
+
attr_reader :key, :value, :previous, :writer, :network_name, :timestamp, :correlation_id
|
|
35
|
+
|
|
36
|
+
# Creates a new MemoryChange instance.
|
|
37
|
+
#
|
|
38
|
+
# @param key [Symbol, String] the memory key that changed
|
|
39
|
+
# @param value [Object] the new value
|
|
40
|
+
# @param previous [Object, nil] the previous value
|
|
41
|
+
# @param writer [String, nil] name of the robot that wrote the value
|
|
42
|
+
# @param network_name [String, nil] name of the network
|
|
43
|
+
# @param timestamp [Time] when the change occurred (defaults to now)
|
|
44
|
+
# @param correlation_id [String, nil] optional correlation ID
|
|
45
|
+
#
|
|
46
|
+
def initialize(key:, value:, previous: nil, writer: nil, network_name: nil, timestamp: nil, correlation_id: nil)
|
|
47
|
+
@key = key.to_sym
|
|
48
|
+
@value = value
|
|
49
|
+
@previous = previous
|
|
50
|
+
@writer = writer
|
|
51
|
+
@network_name = network_name
|
|
52
|
+
@timestamp = timestamp || Time.now
|
|
53
|
+
@correlation_id = correlation_id
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if this is a new key (no previous value).
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
#
|
|
60
|
+
def created?
|
|
61
|
+
@previous.nil? && !@value.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check if this is an update to an existing key.
|
|
65
|
+
#
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
#
|
|
68
|
+
def updated?
|
|
69
|
+
!@previous.nil? && !@value.nil?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if the key was deleted.
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean]
|
|
75
|
+
#
|
|
76
|
+
def deleted?
|
|
77
|
+
@value.nil? && !@previous.nil?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Convert to hash representation.
|
|
81
|
+
#
|
|
82
|
+
# @return [Hash]
|
|
83
|
+
#
|
|
84
|
+
def to_h
|
|
85
|
+
{
|
|
86
|
+
key: @key,
|
|
87
|
+
value: @value,
|
|
88
|
+
previous: @previous,
|
|
89
|
+
writer: @writer,
|
|
90
|
+
network_name: @network_name,
|
|
91
|
+
timestamp: @timestamp.iso8601,
|
|
92
|
+
correlation_id: @correlation_id
|
|
93
|
+
}.compact
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Convert to JSON.
|
|
97
|
+
#
|
|
98
|
+
# @param args [Array] arguments passed to to_json
|
|
99
|
+
# @return [String]
|
|
100
|
+
#
|
|
101
|
+
def to_json(*args)
|
|
102
|
+
to_h.to_json(*args)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Reconstruct from hash.
|
|
106
|
+
#
|
|
107
|
+
# @param hash [Hash] the hash representation
|
|
108
|
+
# @return [MemoryChange]
|
|
109
|
+
#
|
|
110
|
+
def self.from_hash(hash)
|
|
111
|
+
hash = hash.transform_keys(&:to_sym)
|
|
112
|
+
new(
|
|
113
|
+
key: hash[:key],
|
|
114
|
+
value: hash[:value],
|
|
115
|
+
previous: hash[:previous],
|
|
116
|
+
writer: hash[:writer],
|
|
117
|
+
network_name: hash[:network_name],
|
|
118
|
+
timestamp: hash[:timestamp] ? Time.parse(hash[:timestamp]) : nil,
|
|
119
|
+
correlation_id: hash[:correlation_id]
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Base class for all message types in RobotLab
|
|
5
|
+
#
|
|
6
|
+
# Messages represent the communication between users, assistants, and tools
|
|
7
|
+
# in a conversation. This mirrors the TypeScript Message union type.
|
|
8
|
+
#
|
|
9
|
+
# @abstract Subclass and implement specific message types
|
|
10
|
+
#
|
|
11
|
+
class Message
|
|
12
|
+
# Valid message types
|
|
13
|
+
VALID_TYPES = %w[text tool_call tool_result].freeze
|
|
14
|
+
# Valid message roles
|
|
15
|
+
VALID_ROLES = %w[system user assistant tool_result].freeze
|
|
16
|
+
# Valid stop reasons
|
|
17
|
+
VALID_STOP_REASONS = %w[tool stop].freeze
|
|
18
|
+
|
|
19
|
+
# @!attribute [r] type
|
|
20
|
+
# @return [String] the message type (text, tool_call, or tool_result)
|
|
21
|
+
# @!attribute [r] role
|
|
22
|
+
# @return [String] the message role (system, user, assistant, or tool_result)
|
|
23
|
+
# @!attribute [r] content
|
|
24
|
+
# @return [String, Hash, nil] the message content
|
|
25
|
+
# @!attribute [r] stop_reason
|
|
26
|
+
# @return [String, nil] the stop reason (tool or stop)
|
|
27
|
+
attr_reader :type, :role, :content, :stop_reason
|
|
28
|
+
|
|
29
|
+
# Creates a new Message instance.
|
|
30
|
+
#
|
|
31
|
+
# @param type [String, Symbol] the message type
|
|
32
|
+
# @param role [String, Symbol] the message role
|
|
33
|
+
# @param content [String, Hash, nil] the message content
|
|
34
|
+
# @param stop_reason [String, Symbol, nil] the stop reason
|
|
35
|
+
# @raise [ArgumentError] if type, role, or stop_reason is invalid
|
|
36
|
+
def initialize(type:, role:, content:, stop_reason: nil)
|
|
37
|
+
validate_type!(type)
|
|
38
|
+
validate_role!(role)
|
|
39
|
+
validate_stop_reason!(stop_reason) if stop_reason
|
|
40
|
+
|
|
41
|
+
@type = type.to_s
|
|
42
|
+
@role = role.to_s
|
|
43
|
+
@content = content
|
|
44
|
+
@stop_reason = stop_reason&.to_s
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Boolean] true if this is a text message
|
|
48
|
+
def text? = type == "text"
|
|
49
|
+
# @return [Boolean] true if this is a tool call message
|
|
50
|
+
def tool_call? = type == "tool_call"
|
|
51
|
+
# @return [Boolean] true if this is a tool result message
|
|
52
|
+
def tool_result? = type == "tool_result"
|
|
53
|
+
|
|
54
|
+
# @return [Boolean] true if this is a system message
|
|
55
|
+
def system? = role == "system"
|
|
56
|
+
# @return [Boolean] true if this is a user message
|
|
57
|
+
def user? = role == "user"
|
|
58
|
+
# @return [Boolean] true if this is an assistant message
|
|
59
|
+
def assistant? = role == "assistant"
|
|
60
|
+
|
|
61
|
+
# @return [Boolean] true if the conversation stopped naturally
|
|
62
|
+
def stopped? = stop_reason == "stop"
|
|
63
|
+
# @return [Boolean] true if the conversation stopped for a tool call
|
|
64
|
+
def tool_stop? = stop_reason == "tool"
|
|
65
|
+
|
|
66
|
+
# Converts the message to a hash representation.
|
|
67
|
+
#
|
|
68
|
+
# @return [Hash] a hash containing the message data
|
|
69
|
+
def to_h
|
|
70
|
+
{
|
|
71
|
+
type: type,
|
|
72
|
+
role: role,
|
|
73
|
+
content: content,
|
|
74
|
+
stop_reason: stop_reason
|
|
75
|
+
}.compact
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Converts the message to JSON.
|
|
79
|
+
#
|
|
80
|
+
# @param args [Array] arguments passed to to_json
|
|
81
|
+
# @return [String] JSON representation of the message
|
|
82
|
+
def to_json(*args)
|
|
83
|
+
to_h.to_json(*args)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Creates a Message instance from a hash.
|
|
87
|
+
#
|
|
88
|
+
# Automatically determines the appropriate subclass based on the type.
|
|
89
|
+
#
|
|
90
|
+
# @param hash [Hash] the hash representation of a message
|
|
91
|
+
# @return [Message] the appropriate Message subclass instance
|
|
92
|
+
def self.from_hash(hash)
|
|
93
|
+
hash = hash.transform_keys(&:to_sym)
|
|
94
|
+
|
|
95
|
+
case hash[:type]&.to_s
|
|
96
|
+
when "text"
|
|
97
|
+
TextMessage.new(**hash.slice(:role, :content, :stop_reason))
|
|
98
|
+
when "tool_call"
|
|
99
|
+
ToolCallMessage.new(
|
|
100
|
+
role: hash[:role],
|
|
101
|
+
tools: hash[:tools],
|
|
102
|
+
stop_reason: hash[:stop_reason]
|
|
103
|
+
)
|
|
104
|
+
when "tool_result"
|
|
105
|
+
ToolResultMessage.new(
|
|
106
|
+
tool: hash[:tool],
|
|
107
|
+
content: hash[:content],
|
|
108
|
+
stop_reason: hash[:stop_reason]
|
|
109
|
+
)
|
|
110
|
+
else
|
|
111
|
+
new(**hash)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def validate_type!(type)
|
|
118
|
+
return if VALID_TYPES.include?(type.to_s)
|
|
119
|
+
|
|
120
|
+
raise ArgumentError, "Invalid message type: #{type}. Must be one of: #{VALID_TYPES.join(', ')}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def validate_role!(role)
|
|
124
|
+
return if VALID_ROLES.include?(role.to_s)
|
|
125
|
+
|
|
126
|
+
raise ArgumentError, "Invalid role: #{role}. Must be one of: #{VALID_ROLES.join(', ')}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def validate_stop_reason!(stop_reason)
|
|
130
|
+
return if VALID_STOP_REASONS.include?(stop_reason.to_s)
|
|
131
|
+
|
|
132
|
+
raise ArgumentError, "Invalid stop_reason: #{stop_reason}. Must be one of: #{VALID_STOP_REASONS.join(', ')}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Text message from system, user, or assistant
|
|
137
|
+
#
|
|
138
|
+
# @example System message
|
|
139
|
+
# TextMessage.new(role: :system, content: "You are a helpful assistant")
|
|
140
|
+
#
|
|
141
|
+
# @example User message
|
|
142
|
+
# TextMessage.new(role: :user, content: "Hello!")
|
|
143
|
+
#
|
|
144
|
+
# @example Assistant response
|
|
145
|
+
# TextMessage.new(role: :assistant, content: "Hi there!", stop_reason: :stop)
|
|
146
|
+
#
|
|
147
|
+
class TextMessage < Message
|
|
148
|
+
# Creates a new TextMessage instance.
|
|
149
|
+
#
|
|
150
|
+
# @param role [String, Symbol] the message role (system, user, or assistant)
|
|
151
|
+
# @param content [String] the text content
|
|
152
|
+
# @param stop_reason [String, Symbol, nil] the stop reason
|
|
153
|
+
def initialize(role:, content:, stop_reason: nil)
|
|
154
|
+
super(type: "text", role: role, content: content, stop_reason: stop_reason)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Represents a tool/function definition for tool calls
|
|
159
|
+
#
|
|
160
|
+
# @example
|
|
161
|
+
# ToolMessage.new(
|
|
162
|
+
# id: "call_123",
|
|
163
|
+
# name: "get_weather",
|
|
164
|
+
# input: { location: "Berlin" }
|
|
165
|
+
# )
|
|
166
|
+
#
|
|
167
|
+
class ToolMessage
|
|
168
|
+
# @!attribute [r] id
|
|
169
|
+
# @return [String] the unique identifier for this tool call
|
|
170
|
+
# @!attribute [r] name
|
|
171
|
+
# @return [String] the name of the tool being called
|
|
172
|
+
# @!attribute [r] input
|
|
173
|
+
# @return [Hash] the input arguments for the tool
|
|
174
|
+
attr_reader :id, :name, :input
|
|
175
|
+
|
|
176
|
+
# Creates a new ToolMessage instance.
|
|
177
|
+
#
|
|
178
|
+
# @param id [String] the unique identifier for this tool call
|
|
179
|
+
# @param name [String] the name of the tool
|
|
180
|
+
# @param input [Hash, nil] the input arguments
|
|
181
|
+
def initialize(id:, name:, input:)
|
|
182
|
+
@id = id
|
|
183
|
+
@name = name
|
|
184
|
+
@input = input || {}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Converts the tool message to a hash representation.
|
|
188
|
+
#
|
|
189
|
+
# @return [Hash] a hash containing the tool call data
|
|
190
|
+
def to_h
|
|
191
|
+
{
|
|
192
|
+
type: "tool",
|
|
193
|
+
id: id,
|
|
194
|
+
name: name,
|
|
195
|
+
input: input
|
|
196
|
+
}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Converts the tool message to JSON.
|
|
200
|
+
#
|
|
201
|
+
# @param args [Array] arguments passed to to_json
|
|
202
|
+
# @return [String] JSON representation
|
|
203
|
+
def to_json(*args)
|
|
204
|
+
to_h.to_json(*args)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Creates a ToolMessage from a hash.
|
|
208
|
+
#
|
|
209
|
+
# @param hash [Hash] the hash representation
|
|
210
|
+
# @return [ToolMessage]
|
|
211
|
+
def self.from_hash(hash)
|
|
212
|
+
hash = hash.transform_keys(&:to_sym)
|
|
213
|
+
new(
|
|
214
|
+
id: hash[:id],
|
|
215
|
+
name: hash[:name],
|
|
216
|
+
input: hash[:input] || hash[:arguments] || {}
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Message containing one or more tool calls from the assistant
|
|
222
|
+
#
|
|
223
|
+
# @example
|
|
224
|
+
# ToolCallMessage.new(
|
|
225
|
+
# role: :assistant,
|
|
226
|
+
# tools: [
|
|
227
|
+
# ToolMessage.new(id: "call_1", name: "get_weather", input: { location: "Berlin" })
|
|
228
|
+
# ]
|
|
229
|
+
# )
|
|
230
|
+
#
|
|
231
|
+
class ToolCallMessage < Message
|
|
232
|
+
# @!attribute [r] tools
|
|
233
|
+
# @return [Array<ToolMessage>] the tool calls in this message
|
|
234
|
+
attr_reader :tools
|
|
235
|
+
|
|
236
|
+
# Creates a new ToolCallMessage instance.
|
|
237
|
+
#
|
|
238
|
+
# @param role [String, Symbol] the message role (usually assistant)
|
|
239
|
+
# @param tools [Array<ToolMessage, Hash>] the tool calls
|
|
240
|
+
# @param stop_reason [String, Symbol, nil] the stop reason (defaults to "tool")
|
|
241
|
+
def initialize(role:, tools:, stop_reason: nil)
|
|
242
|
+
@tools = normalize_tools(tools)
|
|
243
|
+
super(type: "tool_call", role: role, content: nil, stop_reason: stop_reason || "tool")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Converts the tool call message to a hash representation.
|
|
247
|
+
#
|
|
248
|
+
# @return [Hash] a hash containing the tool call data
|
|
249
|
+
def to_h
|
|
250
|
+
{
|
|
251
|
+
type: type,
|
|
252
|
+
role: role,
|
|
253
|
+
tools: tools.map(&:to_h),
|
|
254
|
+
stop_reason: stop_reason
|
|
255
|
+
}
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
|
|
260
|
+
def normalize_tools(tools)
|
|
261
|
+
tools.map do |tool|
|
|
262
|
+
case tool
|
|
263
|
+
when ToolMessage
|
|
264
|
+
tool
|
|
265
|
+
when Hash
|
|
266
|
+
ToolMessage.from_hash(tool)
|
|
267
|
+
else
|
|
268
|
+
raise ArgumentError, "Invalid tool: must be ToolMessage or Hash"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Result from executing a tool
|
|
275
|
+
#
|
|
276
|
+
# @example Successful result
|
|
277
|
+
# ToolResultMessage.new(
|
|
278
|
+
# tool: ToolMessage.new(id: "call_1", name: "get_weather", input: { location: "Berlin" }),
|
|
279
|
+
# content: { data: { temperature: 15, unit: "celsius" } }
|
|
280
|
+
# )
|
|
281
|
+
#
|
|
282
|
+
# @example Error result
|
|
283
|
+
# ToolResultMessage.new(
|
|
284
|
+
# tool: ToolMessage.new(id: "call_1", name: "get_weather", input: {}),
|
|
285
|
+
# content: { error: "Location is required" }
|
|
286
|
+
# )
|
|
287
|
+
#
|
|
288
|
+
class ToolResultMessage < Message
|
|
289
|
+
# @!attribute [r] tool
|
|
290
|
+
# @return [ToolMessage] the tool call that was executed
|
|
291
|
+
attr_reader :tool
|
|
292
|
+
|
|
293
|
+
# Creates a new ToolResultMessage instance.
|
|
294
|
+
#
|
|
295
|
+
# @param tool [ToolMessage, Hash] the tool call that was executed
|
|
296
|
+
# @param content [Hash] the result content (with :data or :error key)
|
|
297
|
+
# @param stop_reason [String, Symbol, nil] the stop reason (defaults to "tool")
|
|
298
|
+
def initialize(tool:, content:, stop_reason: nil)
|
|
299
|
+
@tool = normalize_tool(tool)
|
|
300
|
+
super(type: "tool_result", role: "tool_result", content: content, stop_reason: stop_reason || "tool")
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Checks if the tool execution was successful.
|
|
304
|
+
#
|
|
305
|
+
# @return [Boolean] true if content contains a :data key
|
|
306
|
+
def success?
|
|
307
|
+
content.is_a?(Hash) && content.key?(:data)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Checks if the tool execution resulted in an error.
|
|
311
|
+
#
|
|
312
|
+
# @return [Boolean] true if content contains an :error key
|
|
313
|
+
def error?
|
|
314
|
+
content.is_a?(Hash) && content.key?(:error)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Returns the result data if successful.
|
|
318
|
+
#
|
|
319
|
+
# @return [Object, nil] the result data, or nil if not successful
|
|
320
|
+
def data
|
|
321
|
+
content[:data] if success?
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Returns the error message if there was an error.
|
|
325
|
+
#
|
|
326
|
+
# @return [String, nil] the error message, or nil if no error
|
|
327
|
+
def error
|
|
328
|
+
content[:error] if error?
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Converts the tool result message to a hash representation.
|
|
332
|
+
#
|
|
333
|
+
# @return [Hash] a hash containing the tool result data
|
|
334
|
+
def to_h
|
|
335
|
+
{
|
|
336
|
+
type: type,
|
|
337
|
+
role: role,
|
|
338
|
+
tool: tool.to_h,
|
|
339
|
+
content: content,
|
|
340
|
+
stop_reason: stop_reason
|
|
341
|
+
}
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
private
|
|
345
|
+
|
|
346
|
+
def normalize_tool(tool)
|
|
347
|
+
case tool
|
|
348
|
+
when ToolMessage
|
|
349
|
+
tool
|
|
350
|
+
when Hash
|
|
351
|
+
ToolMessage.from_hash(tool)
|
|
352
|
+
else
|
|
353
|
+
raise ArgumentError, "Invalid tool: must be ToolMessage or Hash"
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|