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,560 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# LLM-powered robot using ruby_llm-template for prompts
|
|
5
|
+
#
|
|
6
|
+
# Robot is a thin wrapper around RubyLLM.chat that provides:
|
|
7
|
+
# - Template-based prompts via ruby_llm-template
|
|
8
|
+
# - Build-time context (static robot configuration)
|
|
9
|
+
# - Run-time context (per-request dynamic data)
|
|
10
|
+
# - Tool integration via RubyLLM::Tool
|
|
11
|
+
# - Hierarchical MCP and tools configuration
|
|
12
|
+
#
|
|
13
|
+
# == Memory Behavior
|
|
14
|
+
#
|
|
15
|
+
# Robots have two memory contexts depending on how they're used:
|
|
16
|
+
#
|
|
17
|
+
# *Standalone*: Robot uses its own inherent memory (`robot.memory`).
|
|
18
|
+
# Use `robot.reset_memory` to clear it.
|
|
19
|
+
#
|
|
20
|
+
# *In a Network*: Robot uses the network's shared memory (`network.memory`).
|
|
21
|
+
# The robot's inherent memory is ignored. Use `network.reset_memory` to clear it.
|
|
22
|
+
#
|
|
23
|
+
# This allows the same robot instance to work both standalone and as part
|
|
24
|
+
# of a network, with appropriate memory isolation in each context.
|
|
25
|
+
#
|
|
26
|
+
# @example Simple robot with template
|
|
27
|
+
# robot = Robot.new(
|
|
28
|
+
# name: "helper",
|
|
29
|
+
# template: :helper,
|
|
30
|
+
# context: { company_name: "Acme Corp" }
|
|
31
|
+
# )
|
|
32
|
+
# result = robot.run(message: "Hello!", user_name: "Alice")
|
|
33
|
+
#
|
|
34
|
+
# @example Robot with inline system prompt (no template file needed)
|
|
35
|
+
# robot = Robot.new(
|
|
36
|
+
# name: "quick_bot",
|
|
37
|
+
# system_prompt: "You are a helpful assistant. Be concise."
|
|
38
|
+
# )
|
|
39
|
+
#
|
|
40
|
+
# @example Robot with template and additional system prompt
|
|
41
|
+
# robot = Robot.new(
|
|
42
|
+
# name: "support",
|
|
43
|
+
# template: :support_agent,
|
|
44
|
+
# system_prompt: "Today is #{Date.today}. Current promotion: 20% off."
|
|
45
|
+
# )
|
|
46
|
+
#
|
|
47
|
+
# @example Robot with tools
|
|
48
|
+
# robot = Robot.new(
|
|
49
|
+
# name: "support",
|
|
50
|
+
# template: :support,
|
|
51
|
+
# context: { policies: POLICIES },
|
|
52
|
+
# tools: [OrderLookup, RefundProcessor]
|
|
53
|
+
# )
|
|
54
|
+
#
|
|
55
|
+
# @example Robot with hierarchical MCP/tools config
|
|
56
|
+
# robot = Robot.new(
|
|
57
|
+
# name: "assistant",
|
|
58
|
+
# template: :assistant,
|
|
59
|
+
# mcp: :inherit, # Inherit from network/config
|
|
60
|
+
# tools: %w[search_code] # Only allow search_code tool
|
|
61
|
+
# )
|
|
62
|
+
#
|
|
63
|
+
class Robot
|
|
64
|
+
# @!attribute [r] name
|
|
65
|
+
# @return [String] the unique identifier for the robot
|
|
66
|
+
# @!attribute [r] description
|
|
67
|
+
# @return [String, nil] an optional description of the robot's purpose
|
|
68
|
+
# @!attribute [r] template
|
|
69
|
+
# @return [Symbol, nil] the ERB template for the robot's prompt
|
|
70
|
+
# @!attribute [r] system_prompt
|
|
71
|
+
# @return [String, nil] inline system prompt (used alone or appended to template)
|
|
72
|
+
# @!attribute [r] model
|
|
73
|
+
# @return [String, Object] the LLM model identifier or model object
|
|
74
|
+
# @!attribute [r] local_tools
|
|
75
|
+
# @return [Array] the locally defined tools for this robot
|
|
76
|
+
# @!attribute [r] mcp_clients
|
|
77
|
+
# @return [Hash<String, MCP::Client>] connected MCP clients by server name
|
|
78
|
+
# @!attribute [r] mcp_tools
|
|
79
|
+
# @return [Array<Tool>] tools discovered from MCP servers
|
|
80
|
+
# @!attribute [r] memory
|
|
81
|
+
# @return [Memory] the robot's inherent memory (used when standalone, not in network)
|
|
82
|
+
attr_reader :name, :description, :template, :system_prompt, :model, :local_tools, :mcp_clients, :mcp_tools, :memory
|
|
83
|
+
|
|
84
|
+
# @!attribute [r] mcp_config
|
|
85
|
+
# @return [Symbol, Array] build-time MCP configuration (raw, unresolved)
|
|
86
|
+
# @!attribute [r] tools_config
|
|
87
|
+
# @return [Symbol, Array] build-time tools configuration (raw, unresolved)
|
|
88
|
+
attr_reader :mcp_config, :tools_config
|
|
89
|
+
|
|
90
|
+
# Creates a new Robot instance.
|
|
91
|
+
#
|
|
92
|
+
# @param name [String] the unique identifier for the robot
|
|
93
|
+
# @param template [Symbol, nil] the ERB template for the robot's prompt
|
|
94
|
+
# @param system_prompt [String, nil] inline system prompt (can be used alone or with template)
|
|
95
|
+
# @param context [Hash, Proc] variables to pass to the template at build time
|
|
96
|
+
# @param description [String, nil] an optional description of the robot's purpose
|
|
97
|
+
# @param local_tools [Array] tools defined locally for this robot
|
|
98
|
+
# @param model [String, nil] the LLM model to use (defaults to config.default_model)
|
|
99
|
+
# @param mcp_servers [Array] legacy parameter for MCP server configurations
|
|
100
|
+
# @param mcp [Symbol, Array] hierarchical MCP config (:none, :inherit, or array of servers)
|
|
101
|
+
# @param tools [Symbol, Array] hierarchical tools config (:none, :inherit, or array of tool names)
|
|
102
|
+
# @param on_tool_call [Proc, nil] callback invoked when a tool is called
|
|
103
|
+
# @param on_tool_result [Proc, nil] callback invoked when a tool returns a result
|
|
104
|
+
# @param enable_cache [Boolean] whether to enable semantic caching (default: true)
|
|
105
|
+
#
|
|
106
|
+
# @example Basic robot with template
|
|
107
|
+
# Robot.new(name: "helper", template: :helper)
|
|
108
|
+
#
|
|
109
|
+
# @example Robot with inline system prompt
|
|
110
|
+
# Robot.new(name: "bot", system_prompt: "You are helpful.")
|
|
111
|
+
#
|
|
112
|
+
# @example Robot with template and additional system prompt
|
|
113
|
+
# Robot.new(name: "bot", template: :base, system_prompt: "Extra context here.")
|
|
114
|
+
#
|
|
115
|
+
# @example Robot with tools and callbacks
|
|
116
|
+
# Robot.new(
|
|
117
|
+
# name: "support",
|
|
118
|
+
# template: :support,
|
|
119
|
+
# local_tools: [OrderLookup],
|
|
120
|
+
# on_tool_call: ->(call) { puts "Calling #{call.name}" }
|
|
121
|
+
# )
|
|
122
|
+
#
|
|
123
|
+
# @raise [ArgumentError] if neither template nor system_prompt is provided
|
|
124
|
+
def initialize(
|
|
125
|
+
name:,
|
|
126
|
+
template: nil,
|
|
127
|
+
system_prompt: nil,
|
|
128
|
+
context: {},
|
|
129
|
+
description: nil,
|
|
130
|
+
local_tools: [],
|
|
131
|
+
model: nil,
|
|
132
|
+
mcp_servers: [],
|
|
133
|
+
mcp: :none,
|
|
134
|
+
tools: :none,
|
|
135
|
+
on_tool_call: nil,
|
|
136
|
+
on_tool_result: nil,
|
|
137
|
+
enable_cache: true
|
|
138
|
+
)
|
|
139
|
+
unless template || system_prompt
|
|
140
|
+
raise ArgumentError, "Must provide either template or system_prompt"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
@name = name.to_s
|
|
144
|
+
@template = template
|
|
145
|
+
@system_prompt = system_prompt
|
|
146
|
+
@build_context = context
|
|
147
|
+
@description = description
|
|
148
|
+
@local_tools = Array(local_tools)
|
|
149
|
+
@model = model || RobotLab.configuration.default_model
|
|
150
|
+
@on_tool_call = on_tool_call
|
|
151
|
+
@on_tool_result = on_tool_result
|
|
152
|
+
|
|
153
|
+
# Store raw config values for hierarchical resolution
|
|
154
|
+
# mcp_servers is legacy parameter, mcp is the new hierarchical one
|
|
155
|
+
@mcp_config = mcp_servers.any? ? mcp_servers : mcp
|
|
156
|
+
@tools_config = tools
|
|
157
|
+
|
|
158
|
+
# MCP state
|
|
159
|
+
@mcp_clients = {}
|
|
160
|
+
@mcp_tools = []
|
|
161
|
+
@mcp_initialized = false
|
|
162
|
+
|
|
163
|
+
# Inherent memory (used when standalone, not in a network)
|
|
164
|
+
@memory = Memory.new(enable_cache: enable_cache)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Returns the robot's local tools (alias for local_tools).
|
|
168
|
+
#
|
|
169
|
+
# Provided for backward compatibility with earlier API versions.
|
|
170
|
+
#
|
|
171
|
+
# @return [Array] the locally defined tools
|
|
172
|
+
def tools
|
|
173
|
+
@local_tools
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Run the robot with the given context
|
|
177
|
+
#
|
|
178
|
+
# @param network [NetworkRun, nil] Network context if running in network (legacy)
|
|
179
|
+
# @param network_memory [Memory, nil] Shared network memory (preferred)
|
|
180
|
+
# @param memory [Memory, Hash, nil] Runtime memory to merge
|
|
181
|
+
# @param mcp [Symbol, Array, nil] Runtime MCP override (:inherit, :none, nil, [], or array of servers)
|
|
182
|
+
# @param tools [Symbol, Array, nil] Runtime tools override (:inherit, :none, nil, [], or array of tool names)
|
|
183
|
+
# @param run_context [Hash] Context for rendering user template
|
|
184
|
+
# @return [RobotResult]
|
|
185
|
+
#
|
|
186
|
+
# @example Standalone robot with inherent memory
|
|
187
|
+
# robot.run(message: "My name is Alice")
|
|
188
|
+
# robot.run(message: "What's my name?") # Memory persists
|
|
189
|
+
#
|
|
190
|
+
# @example Runtime memory injection
|
|
191
|
+
# robot.run(message: "Hello", memory: { user_id: 123, session: "abc" })
|
|
192
|
+
#
|
|
193
|
+
# @example With network shared memory
|
|
194
|
+
# robot.run(message: "Analyze this", network_memory: network.memory)
|
|
195
|
+
#
|
|
196
|
+
def run(network: nil, network_memory: nil, memory: nil, mcp: :none, tools: :none, **run_context)
|
|
197
|
+
# Determine which memory to use (priority order):
|
|
198
|
+
# 1. Explicit network_memory parameter
|
|
199
|
+
# 2. Network object's memory (legacy)
|
|
200
|
+
# 3. Robot's inherent memory
|
|
201
|
+
run_memory = network_memory || network&.memory || @memory
|
|
202
|
+
|
|
203
|
+
# Merge runtime memory if provided
|
|
204
|
+
case memory
|
|
205
|
+
when Memory
|
|
206
|
+
run_memory = memory
|
|
207
|
+
when Hash
|
|
208
|
+
run_memory.merge!(memory)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Set current_writer so memory notifications know who wrote the value
|
|
212
|
+
previous_writer = run_memory.current_writer
|
|
213
|
+
run_memory.current_writer = @name
|
|
214
|
+
|
|
215
|
+
begin
|
|
216
|
+
# Resolve hierarchical MCP and tools configuration
|
|
217
|
+
resolved_mcp = resolve_mcp_hierarchy(mcp, network: network)
|
|
218
|
+
resolved_tools = resolve_tools_hierarchy(tools, network: network)
|
|
219
|
+
|
|
220
|
+
# Initialize or update MCP clients based on resolved config
|
|
221
|
+
ensure_mcp_clients(resolved_mcp)
|
|
222
|
+
|
|
223
|
+
# Merge build context + run context
|
|
224
|
+
full_context = resolve_context(@build_context, network: network)
|
|
225
|
+
.merge(run_context)
|
|
226
|
+
|
|
227
|
+
# Build chat with template, filtered tools, and semantic cache
|
|
228
|
+
chat = build_chat(full_context, allowed_tools: resolved_tools, memory: run_memory)
|
|
229
|
+
|
|
230
|
+
# Execute and return result
|
|
231
|
+
response = chat.complete
|
|
232
|
+
|
|
233
|
+
build_result(response, run_memory)
|
|
234
|
+
ensure
|
|
235
|
+
# Restore previous writer
|
|
236
|
+
run_memory.current_writer = previous_writer
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# SimpleFlow step interface
|
|
241
|
+
#
|
|
242
|
+
# Allows Robot to be used directly as a step in a SimpleFlow::Pipeline.
|
|
243
|
+
# The robot receives a SimpleFlow::Result, executes, and returns a new
|
|
244
|
+
# SimpleFlow::Result with the robot's output.
|
|
245
|
+
#
|
|
246
|
+
# @param result [SimpleFlow::Result] incoming result from previous step
|
|
247
|
+
# @return [SimpleFlow::Result] result with robot output
|
|
248
|
+
#
|
|
249
|
+
# @example Using a robot as a pipeline step
|
|
250
|
+
# pipeline = SimpleFlow::Pipeline.new do
|
|
251
|
+
# step :classifier, classifier_robot, depends_on: :none
|
|
252
|
+
# step :billing, billing_robot, depends_on: :optional
|
|
253
|
+
# end
|
|
254
|
+
#
|
|
255
|
+
def call(result)
|
|
256
|
+
robot_result = run(**extract_run_context(result))
|
|
257
|
+
|
|
258
|
+
result
|
|
259
|
+
.with_context(@name.to_sym, robot_result)
|
|
260
|
+
.continue(robot_result)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Reset the robot's inherent memory
|
|
264
|
+
#
|
|
265
|
+
# NOTE: This only affects the robot's standalone memory. When a robot runs
|
|
266
|
+
# as part of a network, it uses the network's shared memory instead.
|
|
267
|
+
# To reset memory for network execution, use `network.reset_memory`.
|
|
268
|
+
#
|
|
269
|
+
# @return [self]
|
|
270
|
+
#
|
|
271
|
+
# @example Standalone robot
|
|
272
|
+
# robot.run(message: "My name is Alice")
|
|
273
|
+
# robot.reset_memory # Clears the conversation
|
|
274
|
+
# robot.run(message: "What's my name?") # Won't remember Alice
|
|
275
|
+
#
|
|
276
|
+
# @example Robot in network (reset_memory has no effect on network runs)
|
|
277
|
+
# network.run(message: "Hello")
|
|
278
|
+
# robot.reset_memory # Does NOT affect network memory
|
|
279
|
+
# network.run(message: "Hi") # Network memory still intact
|
|
280
|
+
# network.reset_memory # Use this to reset network memory
|
|
281
|
+
#
|
|
282
|
+
def reset_memory
|
|
283
|
+
@memory.reset
|
|
284
|
+
self
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Disconnect all MCP clients
|
|
288
|
+
#
|
|
289
|
+
# Call this method when done using the robot to clean up MCP connections.
|
|
290
|
+
#
|
|
291
|
+
# @return [self]
|
|
292
|
+
#
|
|
293
|
+
def disconnect
|
|
294
|
+
@mcp_clients.each_value(&:disconnect)
|
|
295
|
+
self
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Converts the robot to a hash representation.
|
|
299
|
+
#
|
|
300
|
+
# @return [Hash] a hash containing the robot's configuration
|
|
301
|
+
def to_h
|
|
302
|
+
{
|
|
303
|
+
name: name,
|
|
304
|
+
description: description,
|
|
305
|
+
template: template,
|
|
306
|
+
system_prompt: system_prompt,
|
|
307
|
+
local_tools: local_tools.map { |t| t.respond_to?(:name) ? t.name : t.to_s },
|
|
308
|
+
mcp_tools: mcp_tools.map(&:name),
|
|
309
|
+
mcp_config: @mcp_config,
|
|
310
|
+
tools_config: @tools_config,
|
|
311
|
+
mcp_servers: @mcp_clients.keys,
|
|
312
|
+
model: model.respond_to?(:model_id) ? model.model_id : model
|
|
313
|
+
}.compact
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
private
|
|
317
|
+
|
|
318
|
+
# Extract run context from SimpleFlow::Result
|
|
319
|
+
#
|
|
320
|
+
# Merges original run params (preserved in context) with current value.
|
|
321
|
+
# Extracts special parameters (mcp, tools, memory, network_memory) for Robot#run.
|
|
322
|
+
#
|
|
323
|
+
# @param result [SimpleFlow::Result] the incoming result
|
|
324
|
+
# @return [Hash] context for run method including mcp/tools config
|
|
325
|
+
#
|
|
326
|
+
def extract_run_context(result)
|
|
327
|
+
run_params = result.context[:run_params] || {}
|
|
328
|
+
|
|
329
|
+
# Extract robot-specific params that should be passed to run()
|
|
330
|
+
mcp = run_params.delete(:mcp) || :none
|
|
331
|
+
tools = run_params.delete(:tools) || :none
|
|
332
|
+
memory = run_params.delete(:memory)
|
|
333
|
+
network_memory = run_params.delete(:network_memory)
|
|
334
|
+
|
|
335
|
+
# Build base context from remaining run params
|
|
336
|
+
base = run_params.dup
|
|
337
|
+
|
|
338
|
+
# Merge current value into context
|
|
339
|
+
merged = case result.value
|
|
340
|
+
when Hash
|
|
341
|
+
base.merge(result.value.transform_keys(&:to_sym))
|
|
342
|
+
when RobotResult
|
|
343
|
+
base.merge(message: result.value.last_text_content)
|
|
344
|
+
when String
|
|
345
|
+
base.merge(message: result.value)
|
|
346
|
+
else
|
|
347
|
+
base.merge(message: result.value.to_s)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Add back the special params for run()
|
|
351
|
+
merged[:mcp] = mcp
|
|
352
|
+
merged[:tools] = tools
|
|
353
|
+
merged[:memory] = memory if memory
|
|
354
|
+
merged[:network_memory] = network_memory if network_memory
|
|
355
|
+
|
|
356
|
+
merged
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def resolve_context(context, network:)
|
|
360
|
+
case context
|
|
361
|
+
when Proc then context.call(network: network)
|
|
362
|
+
when Hash then context
|
|
363
|
+
else {}
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def build_chat(context, allowed_tools:, memory:)
|
|
368
|
+
model_id = @model.respond_to?(:model_id) ? @model.model_id : @model.to_s
|
|
369
|
+
|
|
370
|
+
chat = RubyLLM.chat(model: model_id)
|
|
371
|
+
|
|
372
|
+
# Apply template and/or system_prompt
|
|
373
|
+
# - Template only: use with_template
|
|
374
|
+
# - system_prompt only: use with_instructions
|
|
375
|
+
# - Both: use with_template, then append with_instructions
|
|
376
|
+
if @template
|
|
377
|
+
chat = chat.with_template(@template, **context)
|
|
378
|
+
chat = chat.with_instructions(@system_prompt) if @system_prompt
|
|
379
|
+
else
|
|
380
|
+
chat = chat.with_instructions(@system_prompt)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Get filtered tools based on whitelist
|
|
384
|
+
filtered = filtered_tools(allowed_tools)
|
|
385
|
+
chat = chat.with_tools(*filtered) if filtered.any?
|
|
386
|
+
|
|
387
|
+
# Add callbacks if provided
|
|
388
|
+
chat = chat.on_tool_call(&@on_tool_call) if @on_tool_call
|
|
389
|
+
chat = chat.on_tool_result(&@on_tool_result) if @on_tool_result
|
|
390
|
+
|
|
391
|
+
# NOTE: Semantic cache wrapping is disabled because the SemanticCache::Middleware
|
|
392
|
+
# only supports `ask` method, not `complete`. The caching feature needs to be
|
|
393
|
+
# re-designed to use the `ask` interface or the `fetch` pattern.
|
|
394
|
+
# See: https://github.com/ruby-llm/ruby_llm-semantic_cache
|
|
395
|
+
|
|
396
|
+
chat
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def build_result(response, _memory)
|
|
400
|
+
output = if response.respond_to?(:content) && response.content
|
|
401
|
+
[TextMessage.new(role: "assistant", content: response.content)]
|
|
402
|
+
else
|
|
403
|
+
[]
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
tool_calls = response.respond_to?(:tool_calls) ? (response.tool_calls || []) : []
|
|
407
|
+
|
|
408
|
+
RobotResult.new(
|
|
409
|
+
robot_name: @name,
|
|
410
|
+
output: output,
|
|
411
|
+
tool_calls: normalize_tool_calls(tool_calls),
|
|
412
|
+
stop_reason: response.respond_to?(:stop_reason) ? response.stop_reason : nil
|
|
413
|
+
)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def normalize_tool_calls(tool_calls)
|
|
417
|
+
return [] unless tool_calls
|
|
418
|
+
|
|
419
|
+
tool_calls.map do |tc|
|
|
420
|
+
if tc.is_a?(Hash)
|
|
421
|
+
ToolResultMessage.new(
|
|
422
|
+
tool: tc,
|
|
423
|
+
content: tc[:result] || tc["result"]
|
|
424
|
+
)
|
|
425
|
+
else
|
|
426
|
+
tc
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Resolve MCP hierarchy: runtime -> robot build -> network -> config
|
|
432
|
+
#
|
|
433
|
+
# @param runtime_value [Symbol, Array, nil] Runtime MCP override
|
|
434
|
+
# @param network [NetworkRun, nil] Network context
|
|
435
|
+
# @return [Array] Resolved MCP server configurations
|
|
436
|
+
#
|
|
437
|
+
def resolve_mcp_hierarchy(runtime_value, network:)
|
|
438
|
+
# Get parent value (network or config)
|
|
439
|
+
parent_value = network&.network&.mcp || RobotLab.configuration.mcp
|
|
440
|
+
|
|
441
|
+
# Resolve robot build config against parent
|
|
442
|
+
build_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)
|
|
443
|
+
|
|
444
|
+
# Resolve runtime against build
|
|
445
|
+
ToolConfig.resolve_mcp(runtime_value, parent_value: build_resolved)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Resolve tools hierarchy: runtime -> robot build -> network -> config
|
|
449
|
+
#
|
|
450
|
+
# @param runtime_value [Symbol, Array, nil] Runtime tools override
|
|
451
|
+
# @param network [NetworkRun, nil] Network context
|
|
452
|
+
# @return [Array<String>] Resolved tool names whitelist
|
|
453
|
+
#
|
|
454
|
+
def resolve_tools_hierarchy(runtime_value, network:)
|
|
455
|
+
# Get parent value (network or config)
|
|
456
|
+
parent_value = network&.network&.tools || RobotLab.configuration.tools
|
|
457
|
+
|
|
458
|
+
# Resolve robot build config against parent
|
|
459
|
+
build_resolved = ToolConfig.resolve_tools(@tools_config, parent_value: parent_value)
|
|
460
|
+
|
|
461
|
+
# Resolve runtime against build
|
|
462
|
+
ToolConfig.resolve_tools(runtime_value, parent_value: build_resolved)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Ensure MCP clients are initialized for the given server configs
|
|
466
|
+
#
|
|
467
|
+
# @param mcp_servers [Array] MCP server configurations
|
|
468
|
+
#
|
|
469
|
+
def ensure_mcp_clients(mcp_servers)
|
|
470
|
+
return if mcp_servers.empty?
|
|
471
|
+
|
|
472
|
+
# Get server names from configs
|
|
473
|
+
needed_servers = mcp_servers.map { |s| s.is_a?(Hash) ? s[:name] : s.to_s }.compact
|
|
474
|
+
|
|
475
|
+
# Skip if already initialized with same servers
|
|
476
|
+
return if @mcp_initialized && (@mcp_clients.keys.sort == needed_servers.sort)
|
|
477
|
+
|
|
478
|
+
# Disconnect existing clients if config changed
|
|
479
|
+
disconnect if @mcp_initialized
|
|
480
|
+
|
|
481
|
+
# Initialize new clients
|
|
482
|
+
@mcp_clients = {}
|
|
483
|
+
@mcp_tools = []
|
|
484
|
+
|
|
485
|
+
mcp_servers.each do |server_config|
|
|
486
|
+
init_mcp_client(server_config)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
@mcp_initialized = true
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Initialize a single MCP client
|
|
493
|
+
#
|
|
494
|
+
# @param server_config [Hash] MCP server configuration
|
|
495
|
+
#
|
|
496
|
+
def init_mcp_client(server_config)
|
|
497
|
+
client = MCP::Client.new(server_config)
|
|
498
|
+
client.connect
|
|
499
|
+
|
|
500
|
+
if client.connected?
|
|
501
|
+
server_name = client.server.name
|
|
502
|
+
@mcp_clients[server_name] = client
|
|
503
|
+
discover_mcp_tools(client, server_name)
|
|
504
|
+
else
|
|
505
|
+
RobotLab.configuration.logger.warn(
|
|
506
|
+
"Robot '#{@name}' failed to connect to MCP server: #{server_config[:name] || server_config}"
|
|
507
|
+
)
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Discover tools from an MCP server and add them to @mcp_tools
|
|
512
|
+
#
|
|
513
|
+
# @param client [MCP::Client] Connected MCP client
|
|
514
|
+
# @param server_name [String] Name of the MCP server
|
|
515
|
+
#
|
|
516
|
+
def discover_mcp_tools(client, server_name)
|
|
517
|
+
tools = client.list_tools
|
|
518
|
+
|
|
519
|
+
tools.each do |tool_def|
|
|
520
|
+
tool_name = tool_def[:name]
|
|
521
|
+
mcp_client = client
|
|
522
|
+
|
|
523
|
+
# Create a Tool that delegates to the MCP client
|
|
524
|
+
tool = Tool.new(
|
|
525
|
+
name: tool_name,
|
|
526
|
+
description: tool_def[:description],
|
|
527
|
+
parameters: tool_def[:inputSchema],
|
|
528
|
+
mcp: server_name,
|
|
529
|
+
handler: ->(input, **_opts) { mcp_client.call_tool(tool_name, input) }
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
@mcp_tools << tool
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
RobotLab.configuration.logger.info(
|
|
536
|
+
"Robot '#{@name}' discovered #{tools.size} tools from MCP server '#{server_name}'"
|
|
537
|
+
)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Get all tools (local + MCP)
|
|
541
|
+
#
|
|
542
|
+
# @return [Array] Combined array of local and MCP tools
|
|
543
|
+
#
|
|
544
|
+
def all_tools
|
|
545
|
+
@local_tools + @mcp_tools
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Filter tools based on allowed tool names whitelist
|
|
549
|
+
#
|
|
550
|
+
# @param allowed_names [Array<String>] Whitelist of tool names (empty = all allowed)
|
|
551
|
+
# @return [Array] Filtered tools
|
|
552
|
+
#
|
|
553
|
+
def filtered_tools(allowed_names)
|
|
554
|
+
available = all_tools
|
|
555
|
+
return available if allowed_names.empty?
|
|
556
|
+
|
|
557
|
+
ToolConfig.filter_tools(available, allowed_names: allowed_names)
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
end
|