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,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Handles hierarchical MCP and tools configuration resolution
|
|
5
|
+
#
|
|
6
|
+
# Configuration hierarchy (each level overrides the previous):
|
|
7
|
+
# 1. RobotLab.configuration (global)
|
|
8
|
+
# 2. Network.new (network scope)
|
|
9
|
+
# 3. Robot.new (robot definition scope)
|
|
10
|
+
# 4. robot.run (runtime scope)
|
|
11
|
+
#
|
|
12
|
+
# Value semantics:
|
|
13
|
+
# - :inherit -> Use parent level's configuration
|
|
14
|
+
# - nil -> No items allowed
|
|
15
|
+
# - [] -> No items allowed
|
|
16
|
+
# - :none -> No items allowed
|
|
17
|
+
# - [item1, ...] -> Only these specific items allowed
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# ToolConfig.resolve(:inherit, parent_value: %w[tool1 tool2])
|
|
21
|
+
# # => ["tool1", "tool2"]
|
|
22
|
+
#
|
|
23
|
+
# ToolConfig.resolve(nil, parent_value: %w[tool1 tool2])
|
|
24
|
+
# # => []
|
|
25
|
+
#
|
|
26
|
+
# ToolConfig.resolve(%w[tool3], parent_value: %w[tool1 tool2])
|
|
27
|
+
# # => ["tool3"]
|
|
28
|
+
#
|
|
29
|
+
module ToolConfig
|
|
30
|
+
NONE_VALUES = [nil, [], :none].freeze
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# Resolve a configuration value against its parent
|
|
34
|
+
#
|
|
35
|
+
# @param value [Symbol, Array, nil] The current level's value
|
|
36
|
+
# @param parent_value [Array] The parent level's resolved value
|
|
37
|
+
# @return [Array] The resolved configuration
|
|
38
|
+
#
|
|
39
|
+
def resolve(value, parent_value:)
|
|
40
|
+
return Array(parent_value) if value == :inherit
|
|
41
|
+
return [] if none_value?(value)
|
|
42
|
+
|
|
43
|
+
Array(value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Resolve MCP servers configuration
|
|
47
|
+
#
|
|
48
|
+
# @param value [Symbol, Array, nil] MCP configuration
|
|
49
|
+
# @param parent_value [Array] Parent's MCP servers
|
|
50
|
+
# @return [Array] Resolved MCP server configurations
|
|
51
|
+
#
|
|
52
|
+
def resolve_mcp(value, parent_value:)
|
|
53
|
+
resolve(value, parent_value: parent_value)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Resolve tools configuration
|
|
57
|
+
#
|
|
58
|
+
# @param value [Symbol, Array, nil] Tools configuration (tool names as strings)
|
|
59
|
+
# @param parent_value [Array] Parent's tools
|
|
60
|
+
# @return [Array<String>] Resolved tool names
|
|
61
|
+
#
|
|
62
|
+
def resolve_tools(value, parent_value:)
|
|
63
|
+
resolved = resolve(value, parent_value: parent_value)
|
|
64
|
+
resolved.map(&:to_s)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Check if value represents "no items"
|
|
68
|
+
#
|
|
69
|
+
# @param value [Object] Value to check
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
#
|
|
72
|
+
def none_value?(value)
|
|
73
|
+
NONE_VALUES.include?(value)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if value represents "inherit from parent"
|
|
77
|
+
#
|
|
78
|
+
# @param value [Object] Value to check
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
#
|
|
81
|
+
def inherit_value?(value)
|
|
82
|
+
value == :inherit
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Filter tools based on allowed tool names
|
|
86
|
+
#
|
|
87
|
+
# Given a list of tool objects and a whitelist of tool names,
|
|
88
|
+
# returns only the tools whose names are in the whitelist.
|
|
89
|
+
#
|
|
90
|
+
# @param tools [Array] Tool objects (must respond to :name)
|
|
91
|
+
# @param allowed_names [Array<String>] Whitelist of tool names
|
|
92
|
+
# @return [Array] Filtered tools
|
|
93
|
+
#
|
|
94
|
+
def filter_tools(tools, allowed_names:)
|
|
95
|
+
return [] if allowed_names.empty?
|
|
96
|
+
|
|
97
|
+
allowed_set = allowed_names.map(&:to_s).to_set
|
|
98
|
+
tools.select { |tool| allowed_set.include?(tool_name(tool)) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def tool_name(tool)
|
|
104
|
+
case tool
|
|
105
|
+
when String then tool
|
|
106
|
+
when Symbol then tool.to_s
|
|
107
|
+
else tool.respond_to?(:name) ? tool.name.to_s : tool.to_s
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Registry of tools with lookup by name
|
|
5
|
+
#
|
|
6
|
+
# ToolManifest provides a collection interface for managing multiple tools,
|
|
7
|
+
# with methods for lookup, iteration, and conversion to various formats.
|
|
8
|
+
#
|
|
9
|
+
# @example Creating a manifest
|
|
10
|
+
# manifest = ToolManifest.new([weather_tool, calculator_tool])
|
|
11
|
+
# manifest[:get_weather] # => Tool
|
|
12
|
+
# manifest.names # => ["get_weather", "calculate"]
|
|
13
|
+
#
|
|
14
|
+
class ToolManifest
|
|
15
|
+
include Enumerable
|
|
16
|
+
|
|
17
|
+
# Creates a new ToolManifest instance.
|
|
18
|
+
#
|
|
19
|
+
# @param tools [Array<Tool>] initial tools to add to the manifest
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# manifest = ToolManifest.new([weather_tool, calculator_tool])
|
|
23
|
+
def initialize(tools = [])
|
|
24
|
+
@tools = {}
|
|
25
|
+
Array(tools).each { |tool| add(tool) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Add a tool to the manifest
|
|
29
|
+
#
|
|
30
|
+
# @param tool [Tool] The tool to add
|
|
31
|
+
# @return [self]
|
|
32
|
+
#
|
|
33
|
+
def add(tool)
|
|
34
|
+
@tools[tool.name] = tool
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @!method <<(tool)
|
|
39
|
+
# Alias for {#add}.
|
|
40
|
+
# @param tool [Tool] the tool to add
|
|
41
|
+
# @return [self]
|
|
42
|
+
alias << add
|
|
43
|
+
|
|
44
|
+
# Remove a tool from the manifest
|
|
45
|
+
#
|
|
46
|
+
# @param name [String, Symbol] The tool name
|
|
47
|
+
# @return [Tool, nil] The removed tool
|
|
48
|
+
#
|
|
49
|
+
def remove(name)
|
|
50
|
+
@tools.delete(name.to_s)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get a tool by name
|
|
54
|
+
#
|
|
55
|
+
# @param name [String, Symbol] The tool name
|
|
56
|
+
# @return [Tool, nil]
|
|
57
|
+
#
|
|
58
|
+
def [](name)
|
|
59
|
+
@tools[name.to_s]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get a tool by name, raising if not found
|
|
63
|
+
#
|
|
64
|
+
# @param name [String, Symbol] The tool name
|
|
65
|
+
# @return [Tool]
|
|
66
|
+
# @raise [ToolNotFoundError] If tool doesn't exist
|
|
67
|
+
#
|
|
68
|
+
def fetch(name)
|
|
69
|
+
@tools.fetch(name.to_s) do
|
|
70
|
+
raise ToolNotFoundError, "Tool not found: #{name}. Available tools: #{names.join(', ')}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if a tool exists
|
|
75
|
+
#
|
|
76
|
+
# @param name [String, Symbol] The tool name
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
#
|
|
79
|
+
def include?(name)
|
|
80
|
+
@tools.key?(name.to_s)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @!method has?(name)
|
|
84
|
+
# Alias for {#include?}.
|
|
85
|
+
# @param name [String, Symbol] the tool name
|
|
86
|
+
# @return [Boolean]
|
|
87
|
+
alias has? include?
|
|
88
|
+
|
|
89
|
+
# Get all tool names
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<String>]
|
|
92
|
+
#
|
|
93
|
+
def names
|
|
94
|
+
@tools.keys
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get all tools
|
|
98
|
+
#
|
|
99
|
+
# @return [Array<Tool>]
|
|
100
|
+
#
|
|
101
|
+
def values
|
|
102
|
+
@tools.values
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @!method all
|
|
106
|
+
# Alias for {#values}.
|
|
107
|
+
# @return [Array<Tool>]
|
|
108
|
+
alias all values
|
|
109
|
+
|
|
110
|
+
# @!method to_a
|
|
111
|
+
# Alias for {#values}.
|
|
112
|
+
# @return [Array<Tool>]
|
|
113
|
+
alias to_a values
|
|
114
|
+
|
|
115
|
+
# Number of tools
|
|
116
|
+
#
|
|
117
|
+
# @return [Integer]
|
|
118
|
+
#
|
|
119
|
+
def size
|
|
120
|
+
@tools.size
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @!method count
|
|
124
|
+
# Alias for {#size}.
|
|
125
|
+
# @return [Integer]
|
|
126
|
+
alias count size
|
|
127
|
+
|
|
128
|
+
# @!method length
|
|
129
|
+
# Alias for {#size}.
|
|
130
|
+
# @return [Integer]
|
|
131
|
+
alias length size
|
|
132
|
+
|
|
133
|
+
# Check if manifest is empty
|
|
134
|
+
#
|
|
135
|
+
# @return [Boolean]
|
|
136
|
+
#
|
|
137
|
+
def empty?
|
|
138
|
+
@tools.empty?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Iterate over tools
|
|
142
|
+
#
|
|
143
|
+
# @yield [Tool] Each tool in the manifest
|
|
144
|
+
#
|
|
145
|
+
def each(&block)
|
|
146
|
+
@tools.values.each(&block)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Clear all tools
|
|
150
|
+
#
|
|
151
|
+
# @return [self]
|
|
152
|
+
#
|
|
153
|
+
def clear
|
|
154
|
+
@tools.clear
|
|
155
|
+
self
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Replace all tools
|
|
159
|
+
#
|
|
160
|
+
# @param tools [Array<Tool>] New tools
|
|
161
|
+
# @return [self]
|
|
162
|
+
#
|
|
163
|
+
def replace(tools)
|
|
164
|
+
clear
|
|
165
|
+
Array(tools).each { |tool| add(tool) }
|
|
166
|
+
self
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Merge another manifest or array of tools
|
|
170
|
+
#
|
|
171
|
+
# @param other [ToolManifest, Array<Tool>] Tools to merge
|
|
172
|
+
# @return [self]
|
|
173
|
+
#
|
|
174
|
+
def merge(other)
|
|
175
|
+
case other
|
|
176
|
+
when ToolManifest
|
|
177
|
+
other.each { |tool| add(tool) }
|
|
178
|
+
when Array
|
|
179
|
+
other.each { |tool| add(tool) }
|
|
180
|
+
when Tool
|
|
181
|
+
add(other)
|
|
182
|
+
end
|
|
183
|
+
self
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Convert to hash for JSON Schema
|
|
187
|
+
#
|
|
188
|
+
# @return [Hash] Map of tool names to their JSON schemas
|
|
189
|
+
#
|
|
190
|
+
def to_json_schema
|
|
191
|
+
@tools.transform_values(&:to_json_schema)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Convert to array of ruby_llm Tool classes
|
|
195
|
+
#
|
|
196
|
+
# @return [Array<Class>]
|
|
197
|
+
#
|
|
198
|
+
def to_ruby_llm_tools
|
|
199
|
+
@tools.values.map(&:to_ruby_llm_tool)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Converts the manifest to a hash representation.
|
|
203
|
+
#
|
|
204
|
+
# @return [Hash<String, Hash>] map of tool names to their hash representations
|
|
205
|
+
def to_h
|
|
206
|
+
@tools.transform_values(&:to_h)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Converts the manifest to JSON.
|
|
210
|
+
#
|
|
211
|
+
# @param args [Array] arguments passed to to_json
|
|
212
|
+
# @return [String] JSON representation
|
|
213
|
+
def to_json(*args)
|
|
214
|
+
to_h.to_json(*args)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Create manifest from hash of tool definitions
|
|
218
|
+
#
|
|
219
|
+
# @param hash [Hash] Map of names to tool configs
|
|
220
|
+
# @return [ToolManifest]
|
|
221
|
+
#
|
|
222
|
+
def self.from_hash(hash)
|
|
223
|
+
tools = hash.map do |name, config|
|
|
224
|
+
Tool.new(
|
|
225
|
+
name: name,
|
|
226
|
+
description: config[:description],
|
|
227
|
+
parameters: config[:parameters],
|
|
228
|
+
handler: config[:handler]
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
new(tools)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Enhanced user message with metadata and system prompt augmentation
|
|
5
|
+
#
|
|
6
|
+
# UserMessage wraps the user's input with additional context like
|
|
7
|
+
# thread ID, system prompt additions, and other metadata that can
|
|
8
|
+
# influence robot behavior.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# message = UserMessage.new("What is the weather?")
|
|
12
|
+
# message.content # => "What is the weather?"
|
|
13
|
+
#
|
|
14
|
+
# @example With metadata
|
|
15
|
+
# message = UserMessage.new(
|
|
16
|
+
# "What is the weather?",
|
|
17
|
+
# session_id: "thread_123",
|
|
18
|
+
# system_prompt: "Respond in Spanish",
|
|
19
|
+
# metadata: { user_id: "user_456" }
|
|
20
|
+
# )
|
|
21
|
+
#
|
|
22
|
+
class UserMessage
|
|
23
|
+
# @!attribute [r] content
|
|
24
|
+
# @return [String] the message content
|
|
25
|
+
# @!attribute [r] session_id
|
|
26
|
+
# @return [String, nil] the conversation thread identifier
|
|
27
|
+
# @!attribute [r] system_prompt
|
|
28
|
+
# @return [String, nil] additional system prompt to inject
|
|
29
|
+
# @!attribute [r] metadata
|
|
30
|
+
# @return [Hash] additional metadata
|
|
31
|
+
# @!attribute [r] id
|
|
32
|
+
# @return [String] unique message identifier
|
|
33
|
+
# @!attribute [r] created_at
|
|
34
|
+
# @return [Time] when the message was created
|
|
35
|
+
attr_reader :content, :session_id, :system_prompt, :metadata, :id, :created_at
|
|
36
|
+
|
|
37
|
+
# Creates a new UserMessage instance.
|
|
38
|
+
#
|
|
39
|
+
# @param content [String] the message content
|
|
40
|
+
# @param session_id [String, nil] conversation thread identifier
|
|
41
|
+
# @param system_prompt [String, nil] additional system prompt
|
|
42
|
+
# @param metadata [Hash, nil] additional metadata
|
|
43
|
+
# @param id [String, nil] unique identifier (defaults to UUID)
|
|
44
|
+
def initialize(content, session_id: nil, system_prompt: nil, metadata: nil, id: nil)
|
|
45
|
+
@content = content.to_s
|
|
46
|
+
@session_id = session_id
|
|
47
|
+
@system_prompt = system_prompt
|
|
48
|
+
@metadata = metadata || {}
|
|
49
|
+
@id = id || SecureRandom.uuid
|
|
50
|
+
@created_at = Time.now
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Convert to a simple text message for the conversation
|
|
54
|
+
#
|
|
55
|
+
# @return [TextMessage]
|
|
56
|
+
#
|
|
57
|
+
def to_message
|
|
58
|
+
RobotLab::TextMessage.new(role: "user", content: content)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get the string content (for compatibility with String inputs)
|
|
62
|
+
#
|
|
63
|
+
# @return [String]
|
|
64
|
+
#
|
|
65
|
+
def to_s
|
|
66
|
+
content
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Converts the message to a hash representation.
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash]
|
|
72
|
+
def to_h
|
|
73
|
+
{
|
|
74
|
+
content: content,
|
|
75
|
+
session_id: session_id,
|
|
76
|
+
system_prompt: system_prompt,
|
|
77
|
+
metadata: metadata,
|
|
78
|
+
id: id,
|
|
79
|
+
created_at: created_at.iso8601
|
|
80
|
+
}.compact
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Converts the message to JSON.
|
|
84
|
+
#
|
|
85
|
+
# @param args [Array] arguments passed to to_json
|
|
86
|
+
# @return [String] JSON representation
|
|
87
|
+
def to_json(*args)
|
|
88
|
+
to_h.to_json(*args)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Create from string or hash
|
|
92
|
+
#
|
|
93
|
+
# @param input [String, Hash, UserMessage] Input to normalize
|
|
94
|
+
# @return [UserMessage]
|
|
95
|
+
#
|
|
96
|
+
def self.from(input)
|
|
97
|
+
case input
|
|
98
|
+
when UserMessage
|
|
99
|
+
input
|
|
100
|
+
when String
|
|
101
|
+
new(input)
|
|
102
|
+
when Hash
|
|
103
|
+
input = input.transform_keys(&:to_sym)
|
|
104
|
+
new(
|
|
105
|
+
input[:content],
|
|
106
|
+
session_id: input[:session_id],
|
|
107
|
+
system_prompt: input[:system_prompt],
|
|
108
|
+
metadata: input[:metadata],
|
|
109
|
+
id: input[:id]
|
|
110
|
+
)
|
|
111
|
+
when TextMessage
|
|
112
|
+
new(input.content)
|
|
113
|
+
else
|
|
114
|
+
new(input.to_s)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Thread-safe waiter for blocking get operations on Memory
|
|
5
|
+
#
|
|
6
|
+
# Waiter provides a condition variable wrapper that allows one thread
|
|
7
|
+
# to wait for a value that will be provided by another thread.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# waiter = Waiter.new
|
|
11
|
+
#
|
|
12
|
+
# # In thread A (waiting)
|
|
13
|
+
# value = waiter.wait(timeout: 30)
|
|
14
|
+
#
|
|
15
|
+
# # In thread B (signaling)
|
|
16
|
+
# waiter.signal("the value")
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
class Waiter
|
|
20
|
+
# Creates a new Waiter instance.
|
|
21
|
+
def initialize
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
@condition = ConditionVariable.new
|
|
24
|
+
@value = nil
|
|
25
|
+
@signaled = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Wait for a value to be signaled.
|
|
29
|
+
#
|
|
30
|
+
# @param timeout [Numeric, nil] maximum seconds to wait (nil = indefinite)
|
|
31
|
+
# @return [Object, :timeout] the signaled value, or :timeout if timed out
|
|
32
|
+
#
|
|
33
|
+
def wait(timeout: nil)
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
return @value if @signaled
|
|
36
|
+
|
|
37
|
+
if timeout
|
|
38
|
+
deadline = Time.now + timeout
|
|
39
|
+
until @signaled
|
|
40
|
+
remaining = deadline - Time.now
|
|
41
|
+
return :timeout if remaining <= 0
|
|
42
|
+
@condition.wait(@mutex, remaining)
|
|
43
|
+
end
|
|
44
|
+
@value
|
|
45
|
+
else
|
|
46
|
+
@condition.wait(@mutex) until @signaled
|
|
47
|
+
@value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Signal a value to waiting threads.
|
|
53
|
+
#
|
|
54
|
+
# @param value [Object] the value to signal
|
|
55
|
+
# @return [void]
|
|
56
|
+
#
|
|
57
|
+
def signal(value)
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
@value = value
|
|
60
|
+
@signaled = true
|
|
61
|
+
@condition.broadcast
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check if this waiter has been signaled.
|
|
66
|
+
#
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
#
|
|
69
|
+
def signaled?
|
|
70
|
+
@mutex.synchronize { @signaled }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|