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,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module Streaming
|
|
5
|
+
# Context for managing streaming events during execution
|
|
6
|
+
#
|
|
7
|
+
# StreamingContext provides methods for publishing events with
|
|
8
|
+
# automatic sequencing, timestamping, and ID generation.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# context = Context.new(
|
|
12
|
+
# run_id: "run_123",
|
|
13
|
+
# message_id: "msg_456",
|
|
14
|
+
# scope: "network",
|
|
15
|
+
# publish: ->(event) { broadcast(event) }
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# context.publish_event(event: "text.delta", data: { delta: "Hello" })
|
|
19
|
+
#
|
|
20
|
+
class Context
|
|
21
|
+
# @!attribute [r] run_id
|
|
22
|
+
# @return [String] the unique run identifier
|
|
23
|
+
# @!attribute [r] parent_run_id
|
|
24
|
+
# @return [String, nil] the parent run identifier for nested contexts
|
|
25
|
+
# @!attribute [r] message_id
|
|
26
|
+
# @return [String] the current message identifier
|
|
27
|
+
# @!attribute [r] scope
|
|
28
|
+
# @return [String] the context scope (network, robot, etc.)
|
|
29
|
+
attr_reader :run_id, :parent_run_id, :message_id, :scope
|
|
30
|
+
|
|
31
|
+
# Creates a new streaming Context.
|
|
32
|
+
#
|
|
33
|
+
# @param run_id [String] unique run identifier
|
|
34
|
+
# @param message_id [String] current message identifier
|
|
35
|
+
# @param scope [String, Symbol] context scope
|
|
36
|
+
# @param publish [Proc] callback for publishing events
|
|
37
|
+
# @param parent_run_id [String, nil] parent run identifier
|
|
38
|
+
# @param sequence_counter [SequenceCounter, nil] shared sequence counter
|
|
39
|
+
def initialize(run_id:, message_id:, scope:, publish:, parent_run_id: nil, sequence_counter: nil)
|
|
40
|
+
@run_id = run_id
|
|
41
|
+
@parent_run_id = parent_run_id
|
|
42
|
+
@message_id = message_id
|
|
43
|
+
@scope = scope.to_s
|
|
44
|
+
@publish = publish
|
|
45
|
+
@sequence = sequence_counter || SequenceCounter.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Publish an event
|
|
49
|
+
#
|
|
50
|
+
# @param event [String] Event type
|
|
51
|
+
# @param data [Hash] Event data
|
|
52
|
+
#
|
|
53
|
+
def publish_event(event:, data: {})
|
|
54
|
+
chunk = build_chunk(event, data)
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
@publish.call(chunk)
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
RobotLab.configuration.logger.warn("Streaming error: #{e.message}")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
chunk
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Create a child context for nested robot runs
|
|
66
|
+
#
|
|
67
|
+
# @param robot_run_id [String]
|
|
68
|
+
# @return [Context]
|
|
69
|
+
#
|
|
70
|
+
def create_child_context(robot_run_id)
|
|
71
|
+
Context.new(
|
|
72
|
+
run_id: robot_run_id,
|
|
73
|
+
parent_run_id: @run_id,
|
|
74
|
+
message_id: generate_message_id,
|
|
75
|
+
scope: "robot",
|
|
76
|
+
publish: @publish,
|
|
77
|
+
sequence_counter: @sequence # Share sequence counter
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Create context with shared sequence counter
|
|
82
|
+
#
|
|
83
|
+
# @param run_id [String]
|
|
84
|
+
# @param message_id [String]
|
|
85
|
+
# @param scope [String]
|
|
86
|
+
# @return [Context]
|
|
87
|
+
#
|
|
88
|
+
def create_context_with_shared_sequence(run_id:, message_id:, scope:)
|
|
89
|
+
Context.new(
|
|
90
|
+
run_id: run_id,
|
|
91
|
+
message_id: message_id,
|
|
92
|
+
scope: scope,
|
|
93
|
+
publish: @publish,
|
|
94
|
+
sequence_counter: @sequence
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Generate a part ID (OpenAI-compatible, max 40 chars)
|
|
99
|
+
#
|
|
100
|
+
# @return [String]
|
|
101
|
+
#
|
|
102
|
+
def generate_part_id
|
|
103
|
+
short_msg_id = @message_id[0, 8]
|
|
104
|
+
timestamp = (Time.now.to_f * 1000).to_i.to_s[-6..]
|
|
105
|
+
random = SecureRandom.hex(4)
|
|
106
|
+
"part_#{short_msg_id}_#{timestamp}_#{random}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Generate a step ID for Inngest compatibility
|
|
110
|
+
#
|
|
111
|
+
# @param base_name [String]
|
|
112
|
+
# @return [String]
|
|
113
|
+
#
|
|
114
|
+
def generate_step_id(base_name)
|
|
115
|
+
"publish-#{@sequence.current}:#{base_name}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Generate a new message ID
|
|
119
|
+
#
|
|
120
|
+
# @return [String]
|
|
121
|
+
#
|
|
122
|
+
def generate_message_id
|
|
123
|
+
SecureRandom.uuid
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def build_chunk(event, data)
|
|
129
|
+
seq = @sequence.next
|
|
130
|
+
{
|
|
131
|
+
event: event,
|
|
132
|
+
data: data.merge(
|
|
133
|
+
run_id: @run_id,
|
|
134
|
+
message_id: @message_id,
|
|
135
|
+
scope: @scope
|
|
136
|
+
),
|
|
137
|
+
timestamp: (Time.now.to_f * 1000).to_i,
|
|
138
|
+
sequence_number: seq,
|
|
139
|
+
id: "publish-#{seq}:#{event}"
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module Streaming
|
|
5
|
+
# Event type definitions for streaming
|
|
6
|
+
#
|
|
7
|
+
# Defines the structure and types of events emitted during
|
|
8
|
+
# robot and network execution.
|
|
9
|
+
#
|
|
10
|
+
module Events
|
|
11
|
+
# Run lifecycle events
|
|
12
|
+
RUN_STARTED = "run.started"
|
|
13
|
+
RUN_COMPLETED = "run.completed"
|
|
14
|
+
RUN_FAILED = "run.failed"
|
|
15
|
+
RUN_INTERRUPTED = "run.interrupted"
|
|
16
|
+
|
|
17
|
+
# Step events (for durable execution)
|
|
18
|
+
STEP_STARTED = "step.started"
|
|
19
|
+
STEP_COMPLETED = "step.completed"
|
|
20
|
+
STEP_FAILED = "step.failed"
|
|
21
|
+
|
|
22
|
+
# Part events (message composition)
|
|
23
|
+
PART_CREATED = "part.created"
|
|
24
|
+
PART_COMPLETED = "part.completed"
|
|
25
|
+
PART_FAILED = "part.failed"
|
|
26
|
+
|
|
27
|
+
# Content delta events (token streaming)
|
|
28
|
+
TEXT_DELTA = "text.delta"
|
|
29
|
+
TOOL_CALL_ARGUMENTS_DELTA = "tool_call.arguments.delta"
|
|
30
|
+
TOOL_CALL_OUTPUT_DELTA = "tool_call.output.delta"
|
|
31
|
+
REASONING_DELTA = "reasoning.delta"
|
|
32
|
+
DATA_DELTA = "data.delta"
|
|
33
|
+
|
|
34
|
+
# Human-in-the-loop events
|
|
35
|
+
HITL_REQUESTED = "hitl.requested"
|
|
36
|
+
HITL_RESOLVED = "hitl.resolved"
|
|
37
|
+
|
|
38
|
+
# Metadata events
|
|
39
|
+
USAGE_UPDATED = "usage.updated"
|
|
40
|
+
METADATA_UPDATED = "metadata.updated"
|
|
41
|
+
|
|
42
|
+
# Terminal event
|
|
43
|
+
STREAM_ENDED = "stream.ended"
|
|
44
|
+
|
|
45
|
+
# All event types
|
|
46
|
+
ALL_EVENTS = [
|
|
47
|
+
RUN_STARTED, RUN_COMPLETED, RUN_FAILED, RUN_INTERRUPTED,
|
|
48
|
+
STEP_STARTED, STEP_COMPLETED, STEP_FAILED,
|
|
49
|
+
PART_CREATED, PART_COMPLETED, PART_FAILED,
|
|
50
|
+
TEXT_DELTA, TOOL_CALL_ARGUMENTS_DELTA, TOOL_CALL_OUTPUT_DELTA,
|
|
51
|
+
REASONING_DELTA, DATA_DELTA,
|
|
52
|
+
HITL_REQUESTED, HITL_RESOLVED,
|
|
53
|
+
USAGE_UPDATED, METADATA_UPDATED,
|
|
54
|
+
STREAM_ENDED
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
# Lifecycle events
|
|
58
|
+
LIFECYCLE_EVENTS = [
|
|
59
|
+
RUN_STARTED, RUN_COMPLETED, RUN_FAILED, RUN_INTERRUPTED
|
|
60
|
+
].freeze
|
|
61
|
+
|
|
62
|
+
# Delta events (content streaming)
|
|
63
|
+
DELTA_EVENTS = [
|
|
64
|
+
TEXT_DELTA, TOOL_CALL_ARGUMENTS_DELTA, TOOL_CALL_OUTPUT_DELTA,
|
|
65
|
+
REASONING_DELTA, DATA_DELTA
|
|
66
|
+
].freeze
|
|
67
|
+
|
|
68
|
+
class << self
|
|
69
|
+
# Checks if the event is a lifecycle event.
|
|
70
|
+
#
|
|
71
|
+
# @param event [String] the event type
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
def lifecycle?(event)
|
|
74
|
+
LIFECYCLE_EVENTS.include?(event)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Checks if the event is a delta (content streaming) event.
|
|
78
|
+
#
|
|
79
|
+
# @param event [String] the event type
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def delta?(event)
|
|
82
|
+
DELTA_EVENTS.include?(event)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Checks if the event is a valid event type.
|
|
86
|
+
#
|
|
87
|
+
# @param event [String] the event type
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def valid?(event)
|
|
90
|
+
ALL_EVENTS.include?(event)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module Streaming
|
|
5
|
+
# Monotonic sequence counter for event ordering
|
|
6
|
+
#
|
|
7
|
+
# Provides globally unique, strictly increasing sequence numbers
|
|
8
|
+
# for event ordering across streaming contexts.
|
|
9
|
+
#
|
|
10
|
+
# Thread-safe via Mutex.
|
|
11
|
+
#
|
|
12
|
+
class SequenceCounter
|
|
13
|
+
# Creates a new SequenceCounter.
|
|
14
|
+
#
|
|
15
|
+
# @param start [Integer] the starting value (default: 0)
|
|
16
|
+
def initialize(start: 0)
|
|
17
|
+
@value = start
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get the next sequence number
|
|
22
|
+
#
|
|
23
|
+
# @return [Integer]
|
|
24
|
+
#
|
|
25
|
+
def next
|
|
26
|
+
@mutex.synchronize do
|
|
27
|
+
@value += 1
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get the current value without incrementing
|
|
32
|
+
#
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
#
|
|
35
|
+
def current
|
|
36
|
+
@mutex.synchronize { @value }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Reset to a specific value
|
|
40
|
+
#
|
|
41
|
+
# @param value [Integer]
|
|
42
|
+
#
|
|
43
|
+
def reset(value = 0)
|
|
44
|
+
@mutex.synchronize { @value = value }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Wraps a Robot for use as a pipeline step with per-task configuration
|
|
5
|
+
#
|
|
6
|
+
# Task provides a way to pass step-specific context, MCP servers, tools,
|
|
7
|
+
# and memory to individual robots within a network pipeline. The task's
|
|
8
|
+
# context is deep-merged with the network's run parameters.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic task with context
|
|
11
|
+
# task = Task.new(
|
|
12
|
+
# name: :billing,
|
|
13
|
+
# robot: billing_robot,
|
|
14
|
+
# context: { department: "billing", escalation_level: 2 }
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# @example Task with MCP and tools
|
|
18
|
+
# task = Task.new(
|
|
19
|
+
# name: :developer,
|
|
20
|
+
# robot: dev_robot,
|
|
21
|
+
# context: { project: "api" },
|
|
22
|
+
# mcp: [filesystem_server, github_server],
|
|
23
|
+
# tools: [CodeSearch, FileReader]
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
class Task
|
|
27
|
+
# @!attribute [r] name
|
|
28
|
+
# @return [Symbol] the task/step name
|
|
29
|
+
# @!attribute [r] robot
|
|
30
|
+
# @return [Robot] the wrapped robot instance
|
|
31
|
+
attr_reader :name, :robot
|
|
32
|
+
|
|
33
|
+
# Creates a new Task instance.
|
|
34
|
+
#
|
|
35
|
+
# @param name [Symbol] the task/step name
|
|
36
|
+
# @param robot [Robot] the robot instance to wrap
|
|
37
|
+
# @param context [Hash] task-specific context (deep-merged with run params)
|
|
38
|
+
# @param mcp [Symbol, Array] MCP server config (:none, :inherit, or array)
|
|
39
|
+
# @param tools [Symbol, Array] tools config (:none, :inherit, or array)
|
|
40
|
+
# @param memory [Memory, Hash, nil] task-specific memory
|
|
41
|
+
#
|
|
42
|
+
def initialize(name:, robot:, context: {}, mcp: :none, tools: :none, memory: nil)
|
|
43
|
+
@name = name.to_sym
|
|
44
|
+
@robot = robot
|
|
45
|
+
@context = context
|
|
46
|
+
@mcp = mcp
|
|
47
|
+
@tools = tools
|
|
48
|
+
@memory = memory
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# SimpleFlow step interface
|
|
52
|
+
#
|
|
53
|
+
# Enhances the result's run_params with task-specific configuration
|
|
54
|
+
# before delegating to the wrapped robot.
|
|
55
|
+
#
|
|
56
|
+
# @param result [SimpleFlow::Result] incoming result from previous step
|
|
57
|
+
# @return [SimpleFlow::Result] result with robot output
|
|
58
|
+
#
|
|
59
|
+
def call(result)
|
|
60
|
+
# Get current run params and deep merge with task context
|
|
61
|
+
run_params = deep_merge(
|
|
62
|
+
result.context[:run_params] || {},
|
|
63
|
+
@context
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Add task-specific robot config
|
|
67
|
+
run_params[:mcp] = @mcp unless @mcp == :none
|
|
68
|
+
run_params[:tools] = @tools unless @tools == :none
|
|
69
|
+
run_params[:memory] = @memory if @memory
|
|
70
|
+
|
|
71
|
+
# Create enhanced result with merged params
|
|
72
|
+
enhanced_result = result.with_context(:run_params, run_params)
|
|
73
|
+
|
|
74
|
+
# Delegate to robot
|
|
75
|
+
@robot.call(enhanced_result)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Converts the task to a hash representation.
|
|
79
|
+
#
|
|
80
|
+
# @return [Hash]
|
|
81
|
+
#
|
|
82
|
+
def to_h
|
|
83
|
+
{
|
|
84
|
+
name: @name,
|
|
85
|
+
robot: @robot.name,
|
|
86
|
+
context: @context,
|
|
87
|
+
mcp: @mcp,
|
|
88
|
+
tools: @tools,
|
|
89
|
+
memory: @memory ? true : nil
|
|
90
|
+
}.compact
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Deep merge two hashes
|
|
96
|
+
#
|
|
97
|
+
# Values from `override` take precedence. Nested hashes are merged
|
|
98
|
+
# recursively. Arrays are replaced, not concatenated.
|
|
99
|
+
#
|
|
100
|
+
# @param base [Hash] the base hash
|
|
101
|
+
# @param override [Hash] the overriding hash
|
|
102
|
+
# @return [Hash] the merged result
|
|
103
|
+
#
|
|
104
|
+
def deep_merge(base, override)
|
|
105
|
+
base = base.transform_keys(&:to_sym)
|
|
106
|
+
override = override.transform_keys(&:to_sym)
|
|
107
|
+
|
|
108
|
+
base.merge(override) do |_key, old_val, new_val|
|
|
109
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
110
|
+
deep_merge(old_val, new_val)
|
|
111
|
+
else
|
|
112
|
+
new_val
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Defines a tool/function that robots can use
|
|
5
|
+
#
|
|
6
|
+
# Tools are capabilities that robots can invoke during execution.
|
|
7
|
+
# They have a name, description, parameter schema, and handler.
|
|
8
|
+
#
|
|
9
|
+
# @example Simple tool
|
|
10
|
+
# tool = Tool.new(
|
|
11
|
+
# name: "get_time",
|
|
12
|
+
# description: "Get the current time",
|
|
13
|
+
# handler: -> (_input, **_opts) { Time.now.to_s }
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# @example Tool with parameters (using ruby_llm-schema)
|
|
17
|
+
# class WeatherParams < RubyLLM::Schema
|
|
18
|
+
# string :location, description: "City name"
|
|
19
|
+
# string :unit, enum: %w[celsius fahrenheit], required: false
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# tool = Tool.new(
|
|
23
|
+
# name: "get_weather",
|
|
24
|
+
# description: "Get weather for a location",
|
|
25
|
+
# parameters: WeatherParams,
|
|
26
|
+
# handler: ->(input, **opts) {
|
|
27
|
+
# # input[:location], input[:unit] are validated
|
|
28
|
+
# fetch_weather(input[:location], input[:unit] || "celsius")
|
|
29
|
+
# }
|
|
30
|
+
# )
|
|
31
|
+
#
|
|
32
|
+
class Tool
|
|
33
|
+
# @!attribute [r] name
|
|
34
|
+
# @return [String] the unique identifier for the tool
|
|
35
|
+
# @!attribute [r] description
|
|
36
|
+
# @return [String, nil] a description of what the tool does
|
|
37
|
+
# @!attribute [r] parameters
|
|
38
|
+
# @return [Class, Hash, nil] the parameter schema (RubyLLM::Schema or JSON Schema hash)
|
|
39
|
+
# @!attribute [r] handler
|
|
40
|
+
# @return [Proc, nil] the callable that executes the tool logic
|
|
41
|
+
# @!attribute [r] mcp
|
|
42
|
+
# @return [String, nil] the MCP server name if this is an MCP-provided tool
|
|
43
|
+
# @!attribute [r] strict
|
|
44
|
+
# @return [Boolean, nil] whether strict mode is enabled
|
|
45
|
+
attr_reader :name, :description, :parameters, :handler, :mcp, :strict
|
|
46
|
+
|
|
47
|
+
# Creates a new Tool instance.
|
|
48
|
+
#
|
|
49
|
+
# @param name [String] the unique identifier for the tool
|
|
50
|
+
# @param description [String, nil] a description of what the tool does
|
|
51
|
+
# @param parameters [Class, Hash, nil] parameter schema (RubyLLM::Schema or JSON Schema)
|
|
52
|
+
# @param handler [Proc, nil] the callable that executes the tool logic
|
|
53
|
+
# @param mcp [String, nil] MCP server name if this is an MCP tool
|
|
54
|
+
# @param strict [Boolean, nil] whether strict mode is enabled
|
|
55
|
+
# @yield [input, **opts] optional block as handler
|
|
56
|
+
#
|
|
57
|
+
# @example Tool with block handler
|
|
58
|
+
# Tool.new(name: "greet", description: "Greet user") do |input, **opts|
|
|
59
|
+
# "Hello, #{input[:name]}!"
|
|
60
|
+
# end
|
|
61
|
+
def initialize(name:, description: nil, parameters: nil, handler: nil, mcp: nil, strict: nil, &block)
|
|
62
|
+
@name = name.to_s
|
|
63
|
+
@description = description
|
|
64
|
+
@parameters = parameters
|
|
65
|
+
@handler = handler || block
|
|
66
|
+
@mcp = mcp
|
|
67
|
+
@strict = strict
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Execute the tool with input and context
|
|
71
|
+
#
|
|
72
|
+
# Supports two calling conventions:
|
|
73
|
+
# - Direct: tool.call(input, robot: robot, network: network)
|
|
74
|
+
# - ruby_llm: tool.call(args) - called without keyword args
|
|
75
|
+
#
|
|
76
|
+
# @param input [Hash] The input parameters (validated against schema)
|
|
77
|
+
# @param robot [Robot, nil] The robot invoking this tool
|
|
78
|
+
# @param network [NetworkRun, nil] The network context if running in a network
|
|
79
|
+
# @param step [Object, nil] Durable execution step context
|
|
80
|
+
# @return [Object] The tool's output
|
|
81
|
+
#
|
|
82
|
+
def call(input, robot: nil, network: nil, step: nil)
|
|
83
|
+
raise Error, "Tool '#{name}' has no handler defined" unless handler
|
|
84
|
+
|
|
85
|
+
validated_input = validate_input(input)
|
|
86
|
+
handler.call(validated_input, robot: robot, network: network, step: step)
|
|
87
|
+
rescue Error
|
|
88
|
+
raise
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
{ error: Errors.serialize(e) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Convert to JSON Schema for LLM function calling
|
|
94
|
+
#
|
|
95
|
+
# @return [Hash] JSON Schema representation
|
|
96
|
+
#
|
|
97
|
+
def to_json_schema
|
|
98
|
+
schema = if parameters.respond_to?(:to_json_schema)
|
|
99
|
+
# ruby_llm-schema class
|
|
100
|
+
parameters.new.to_json_schema[:schema]
|
|
101
|
+
elsif parameters.is_a?(Hash)
|
|
102
|
+
# Raw JSON schema
|
|
103
|
+
parameters
|
|
104
|
+
else
|
|
105
|
+
# No parameters
|
|
106
|
+
{ type: "object", properties: {}, required: [] }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
{
|
|
110
|
+
name: name,
|
|
111
|
+
description: description,
|
|
112
|
+
parameters: schema
|
|
113
|
+
}.compact
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Convert to ruby_llm Tool class for integration
|
|
117
|
+
#
|
|
118
|
+
# @return [Class] A RubyLLM::Tool subclass
|
|
119
|
+
#
|
|
120
|
+
def to_ruby_llm_tool
|
|
121
|
+
tool = self
|
|
122
|
+
Class.new(RubyLLM::Tool) do
|
|
123
|
+
description tool.description
|
|
124
|
+
|
|
125
|
+
# Define parameters from schema
|
|
126
|
+
if tool.parameters.respond_to?(:to_json_schema)
|
|
127
|
+
schema = tool.parameters.new.to_json_schema[:schema]
|
|
128
|
+
schema[:properties]&.each do |prop_name, prop_def|
|
|
129
|
+
param prop_name.to_sym,
|
|
130
|
+
type: prop_def[:type],
|
|
131
|
+
desc: prop_def[:description],
|
|
132
|
+
required: schema[:required]&.include?(prop_name.to_s)
|
|
133
|
+
end
|
|
134
|
+
elsif tool.parameters.is_a?(Hash) && tool.parameters[:properties]
|
|
135
|
+
tool.parameters[:properties].each do |prop_name, prop_def|
|
|
136
|
+
param prop_name.to_sym,
|
|
137
|
+
type: prop_def[:type] || "string",
|
|
138
|
+
desc: prop_def[:description],
|
|
139
|
+
required: tool.parameters[:required]&.include?(prop_name.to_s)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
define_method(:execute) do |**kwargs|
|
|
144
|
+
# This will be overridden at runtime with proper context
|
|
145
|
+
kwargs
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Converts the tool to a hash representation.
|
|
151
|
+
#
|
|
152
|
+
# @return [Hash] a hash containing the tool configuration
|
|
153
|
+
def to_h
|
|
154
|
+
{
|
|
155
|
+
name: name,
|
|
156
|
+
description: description,
|
|
157
|
+
parameters: parameters_to_hash,
|
|
158
|
+
mcp: mcp,
|
|
159
|
+
strict: strict
|
|
160
|
+
}.compact
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Converts the tool to JSON.
|
|
164
|
+
#
|
|
165
|
+
# @param args [Array] arguments passed to to_json
|
|
166
|
+
# @return [String] JSON representation of the tool
|
|
167
|
+
def to_json(*args)
|
|
168
|
+
to_h.to_json(*args)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check if this is an MCP-provided tool
|
|
172
|
+
#
|
|
173
|
+
# @return [Boolean]
|
|
174
|
+
#
|
|
175
|
+
def mcp?
|
|
176
|
+
!mcp.nil?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Return parameters schema for ruby_llm compatibility
|
|
180
|
+
#
|
|
181
|
+
# @return [Hash, nil] JSON Schema for tool parameters
|
|
182
|
+
#
|
|
183
|
+
def params_schema
|
|
184
|
+
if parameters.respond_to?(:to_json_schema)
|
|
185
|
+
parameters.new.to_json_schema[:schema]
|
|
186
|
+
elsif parameters.is_a?(Hash)
|
|
187
|
+
parameters
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Provider-specific parameters for ruby_llm compatibility
|
|
192
|
+
#
|
|
193
|
+
# @return [Hash] Empty hash (no provider-specific params)
|
|
194
|
+
#
|
|
195
|
+
def provider_params
|
|
196
|
+
{}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def validate_input(input)
|
|
202
|
+
return input unless parameters
|
|
203
|
+
|
|
204
|
+
input = input.transform_keys(&:to_sym) if input.is_a?(Hash)
|
|
205
|
+
|
|
206
|
+
if parameters.respond_to?(:new) && parameters.ancestors.include?(defined?(RubyLLM::Schema) ? RubyLLM::Schema : Object)
|
|
207
|
+
# Validate with ruby_llm-schema (if available)
|
|
208
|
+
# For now, just pass through
|
|
209
|
+
input
|
|
210
|
+
else
|
|
211
|
+
input
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def parameters_to_hash
|
|
216
|
+
if parameters.respond_to?(:to_json_schema)
|
|
217
|
+
parameters.new.to_json_schema
|
|
218
|
+
elsif parameters.is_a?(Hash)
|
|
219
|
+
parameters
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|