robot_lab 0.2.8 → 0.3.0
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 +4 -4
- data/.envrc +2 -2
- data/CHANGELOG.md +53 -0
- data/CLAUDE.md +4 -2
- data/README.md +15 -17
- data/docs/architecture/core-concepts.md +1 -1
- data/docs/architecture/state-management.md +4 -4
- data/docs/concepts.md +1 -1
- data/docs/getting-started/installation.md +1 -4
- data/docs/guides/memory.md +2 -2
- data/examples/.envrc +2 -0
- data/examples/02_tools.rb +8 -8
- data/examples/03_network.rb +1 -1
- data/examples/04_mcp.rb +7 -5
- data/examples/08_llm_config.rb +5 -5
- data/examples/09_chaining.rb +3 -3
- data/examples/14_rusty_circuit/comic.rb +8 -8
- data/examples/14_rusty_circuit/scout.rb +4 -4
- data/examples/15_memory_network_and_bus/README.md +66 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +5 -7
- data/examples/15_memory_network_and_bus/output/final_article.md +5 -10
- data/examples/15_memory_network_and_bus/output/linux_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/memory.json +6 -6
- data/examples/15_memory_network_and_bus/output/revision_1.md +21 -10
- data/examples/15_memory_network_and_bus/output/revision_2.md +43 -6
- data/examples/15_memory_network_and_bus/output/revision_3.md +8 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
- data/examples/16_writers_room/tools.rb +14 -14
- data/examples/19_token_tracking.rb +2 -2
- data/examples/20_circuit_breaker.rb +3 -3
- data/examples/22_context_compression.rb +1 -1
- data/examples/27_incident_response/README.md +65 -0
- data/examples/28_mcp_discovery.rb +2 -2
- data/examples/29_ractor_tools.rb +4 -4
- data/examples/30_ractor_network.rb +2 -2
- data/examples/33_stock_predictor.rb +8 -8
- data/examples/35_hooks.rb +3 -3
- data/examples/README.md +17 -0
- data/examples/common.rb +55 -23
- data/examples/run_all.rb +60 -0
- data/lib/robot_lab/ask_user.rb +3 -3
- data/lib/robot_lab/config/defaults.yml +5 -5
- data/lib/robot_lab/config.rb +2 -2
- data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -3
- data/lib/robot_lab/memory.rb +16 -7
- data/lib/robot_lab/robot/hooking.rb +26 -0
- data/lib/robot_lab/robot/result_building.rb +119 -0
- data/lib/robot_lab/robot.rb +37 -120
- data/lib/robot_lab/run_config.rb +52 -20
- data/lib/robot_lab/tool.rb +7 -12
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +3 -3
- metadata +18 -27
data/lib/robot_lab/robot.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative 'robot/history_search'
|
|
|
7
7
|
require_relative 'robot/agent_skill_matching'
|
|
8
8
|
require_relative 'robot/budget'
|
|
9
9
|
require_relative 'robot/hooking'
|
|
10
|
+
require_relative 'robot/result_building'
|
|
10
11
|
|
|
11
12
|
module RobotLab
|
|
12
13
|
# LLM-powered robot built on RubyLLM::Agent
|
|
@@ -54,6 +55,7 @@ module RobotLab
|
|
|
54
55
|
include Robot::HistorySearch
|
|
55
56
|
include Robot::Budget
|
|
56
57
|
include Robot::Hooking
|
|
58
|
+
include Robot::ResultBuilding
|
|
57
59
|
include Runnable
|
|
58
60
|
prepend Robot::AgentSkillMatching
|
|
59
61
|
|
|
@@ -367,9 +369,7 @@ module RobotLab
|
|
|
367
369
|
# access to conversation state.
|
|
368
370
|
#
|
|
369
371
|
# @return [RubyLLM::Chat]
|
|
370
|
-
|
|
371
|
-
@chat
|
|
372
|
-
end
|
|
372
|
+
attr_reader :chat
|
|
373
373
|
|
|
374
374
|
# Return the conversation messages from the underlying chat.
|
|
375
375
|
#
|
|
@@ -386,10 +386,10 @@ module RobotLab
|
|
|
386
386
|
def clear_messages(keep_system: true)
|
|
387
387
|
if keep_system
|
|
388
388
|
system_msg = @chat.messages.find { |m| m.role == :system }
|
|
389
|
-
@chat.
|
|
389
|
+
@chat.messages = []
|
|
390
390
|
@chat.add_message(system_msg) if system_msg
|
|
391
391
|
else
|
|
392
|
-
@chat.
|
|
392
|
+
@chat.messages = []
|
|
393
393
|
end
|
|
394
394
|
self
|
|
395
395
|
end
|
|
@@ -399,8 +399,8 @@ module RobotLab
|
|
|
399
399
|
# @param messages [Array<RubyLLM::Message>] the messages to restore
|
|
400
400
|
# @return [self]
|
|
401
401
|
def replace_messages(messages)
|
|
402
|
-
@chat.
|
|
403
|
-
messages.each { |m| @chat.add_message(m) }
|
|
402
|
+
@chat.messages = []
|
|
403
|
+
messages.each { |m| @chat.add_message(coerce_replacement_message(m)) }
|
|
404
404
|
self
|
|
405
405
|
end
|
|
406
406
|
|
|
@@ -665,6 +665,8 @@ module RobotLab
|
|
|
665
665
|
@learnings = []
|
|
666
666
|
@hooks = HookRegistry.new
|
|
667
667
|
@budget_ledger = build_budget_ledger
|
|
668
|
+
@circuit_breaker_armed = false
|
|
669
|
+
@circuit_breaker_call_count = 0
|
|
668
670
|
end
|
|
669
671
|
|
|
670
672
|
def initialize_memory
|
|
@@ -716,20 +718,33 @@ module RobotLab
|
|
|
716
718
|
|
|
717
719
|
def apply_chat_params
|
|
718
720
|
@chat.with_temperature(@config.temperature) if @config.temperature
|
|
721
|
+
@chat.with_max_output_tokens(@config.max_tokens) if @config.max_tokens
|
|
719
722
|
|
|
723
|
+
# ruby_llm 2.0 has no with_params; provider-specific sampling knobs are
|
|
724
|
+
# merged into the request payload via with_provider_options instead.
|
|
725
|
+
# with_provider_options replaces prior options, so merge with whatever
|
|
726
|
+
# the chat already carries (e.g. from template front matter).
|
|
720
727
|
extra_params = {
|
|
721
728
|
top_p: @config.top_p, top_k: @config.top_k,
|
|
722
|
-
max_tokens: @config.max_tokens,
|
|
723
729
|
presence_penalty: @config.presence_penalty,
|
|
724
730
|
frequency_penalty: @config.frequency_penalty,
|
|
725
731
|
stop: @config.stop
|
|
726
732
|
}.compact
|
|
727
|
-
@chat.
|
|
733
|
+
@chat.with_provider_options(@chat.provider_options.to_h.merge(extra_params)) if extra_params.any?
|
|
728
734
|
end
|
|
729
735
|
|
|
736
|
+
# ruby_llm 2.0 callbacks (before_tool_call/after_tool_result) are additive
|
|
737
|
+
# and cannot be removed, so register a single dispatcher per chat that
|
|
738
|
+
# consults the robot's current state. The per-run circuit breaker arms and
|
|
739
|
+
# disarms a flag instead of swapping callbacks.
|
|
730
740
|
def register_chat_callbacks
|
|
731
|
-
@chat.
|
|
732
|
-
|
|
741
|
+
@chat.before_tool_call do |tool_call|
|
|
742
|
+
enforce_circuit_breaker! if @circuit_breaker_armed
|
|
743
|
+
@on_tool_call&.call(tool_call)
|
|
744
|
+
end
|
|
745
|
+
@chat.after_tool_result do |result|
|
|
746
|
+
@on_tool_result&.call(result)
|
|
747
|
+
end
|
|
733
748
|
setup_bus_channel if @bus
|
|
734
749
|
end
|
|
735
750
|
|
|
@@ -761,6 +776,7 @@ module RobotLab
|
|
|
761
776
|
end
|
|
762
777
|
end
|
|
763
778
|
|
|
779
|
+
# :reek:TooManyStatements -- resolve/discover/connect/filter/attach is one linear per-turn tool pipeline.
|
|
764
780
|
def prepare_tools(message:, mcp:, tools:, network:, network_config:)
|
|
765
781
|
resolved_mcp = resolve_mcp_hierarchy(mcp, network: network, network_config: network_config)
|
|
766
782
|
resolved_tools = resolve_tools_hierarchy(tools, network: network, network_config: network_config)
|
|
@@ -779,12 +795,16 @@ module RobotLab
|
|
|
779
795
|
# runtime value before resolution to honor the zero-tools intent.
|
|
780
796
|
filtered = explicit_none_tools?(tools) ? [] : cap_tools(filtered_tools(resolved_tools))
|
|
781
797
|
|
|
782
|
-
#
|
|
783
|
-
# RubyLLM's with_tools appends
|
|
784
|
-
#
|
|
785
|
-
#
|
|
786
|
-
#
|
|
787
|
-
|
|
798
|
+
# Clear-then-add so the chat holds EXACTLY this turn's resolved+capped
|
|
799
|
+
# set. RubyLLM's with_tools appends; on a persistent chat that lets tools
|
|
800
|
+
# accumulate across turns, so a capped per-turn addition could still push
|
|
801
|
+
# the chat's total past the provider limit. with_tools(nil) is ruby_llm
|
|
802
|
+
# 2.0's "clear the tool set"; an explicit none stops there, sending zero
|
|
803
|
+
# tools this turn.
|
|
804
|
+
return unless filtered.any? || explicit_none_tools?(tools)
|
|
805
|
+
|
|
806
|
+
@chat.with_tools(nil)
|
|
807
|
+
@chat.with_tools(*filtered) if filtered.any?
|
|
788
808
|
end
|
|
789
809
|
|
|
790
810
|
# True when the runtime tools value explicitly requests zero tools — `:none`
|
|
@@ -902,85 +922,6 @@ module RobotLab
|
|
|
902
922
|
merged
|
|
903
923
|
end
|
|
904
924
|
|
|
905
|
-
# :reek:TooManyStatements :reek:FeatureEnvy -- adapting a provider response's many optional fields into
|
|
906
|
-
# a RobotResult is inherently response-centric.
|
|
907
|
-
def build_result(response, _memory)
|
|
908
|
-
text = result_text(response)
|
|
909
|
-
output = text ? [TextMessage.new(role: 'assistant', content: text)] : []
|
|
910
|
-
|
|
911
|
-
tool_calls = response.respond_to?(:tool_calls) ? (response.tool_calls || []) : []
|
|
912
|
-
|
|
913
|
-
# Extract token usage from the response
|
|
914
|
-
input_toks = output_toks = 0
|
|
915
|
-
if response.respond_to?(:tokens) && (tokens = response.tokens)
|
|
916
|
-
input_toks = tokens.input.to_i
|
|
917
|
-
output_toks = tokens.output.to_i
|
|
918
|
-
elsif response.respond_to?(:input_tokens)
|
|
919
|
-
input_toks = response.input_tokens.to_i
|
|
920
|
-
output_toks = response.respond_to?(:output_tokens) ? response.output_tokens.to_i : 0
|
|
921
|
-
end
|
|
922
|
-
|
|
923
|
-
@total_input_tokens += input_toks
|
|
924
|
-
@total_output_tokens += output_toks
|
|
925
|
-
|
|
926
|
-
RobotResult.new(
|
|
927
|
-
robot_name: @name,
|
|
928
|
-
output: output,
|
|
929
|
-
tool_calls: normalize_tool_calls(tool_calls),
|
|
930
|
-
stop_reason: response.respond_to?(:stop_reason) ? response.stop_reason : nil,
|
|
931
|
-
raw: response,
|
|
932
|
-
input_tokens: input_toks,
|
|
933
|
-
output_tokens: output_toks
|
|
934
|
-
)
|
|
935
|
-
end
|
|
936
|
-
|
|
937
|
-
# Text for the result's output. Prefers the final response's content, then
|
|
938
|
-
# falls back in order to: (1) thinking text for models that route all output
|
|
939
|
-
# through reasoning_content (e.g. qwen3 on Ollama), (2) the most recent
|
|
940
|
-
# assistant text within the current turn for models that end on a tool call
|
|
941
|
-
# with no trailing text.
|
|
942
|
-
#
|
|
943
|
-
# The chat-history fallback is scoped to messages AFTER the last user message
|
|
944
|
-
# (the current turn) to prevent a previous turn's response from being returned
|
|
945
|
-
# when a thinking-mode model emits nothing in response.content.
|
|
946
|
-
# :reek:TooManyStatements :reek:FeatureEnvy -- documented fallback chain over the response's optional content/thinking/history fields.
|
|
947
|
-
def result_text(response)
|
|
948
|
-
content = response.content if response.respond_to?(:content)
|
|
949
|
-
return content if content && !content.to_s.empty?
|
|
950
|
-
|
|
951
|
-
# Ollama routes qwen3's reasoning to reasoning_content, which ruby_llm
|
|
952
|
-
# surfaces as response.thinking (a RubyLLM::Thinking object). When content
|
|
953
|
-
# is nil and thinking is present, the thinking IS the response for that turn.
|
|
954
|
-
if response.respond_to?(:thinking) && (thinking = response.thinking)
|
|
955
|
-
thinking_text = thinking.respond_to?(:text) ? thinking.text.to_s : thinking.to_s
|
|
956
|
-
return thinking_text unless thinking_text.empty?
|
|
957
|
-
end
|
|
958
|
-
|
|
959
|
-
return nil unless @chat.respond_to?(:messages)
|
|
960
|
-
|
|
961
|
-
messages = @chat.messages
|
|
962
|
-
last_user_idx = messages.rindex { |m| m.role == :user } || -1
|
|
963
|
-
current_turn = messages[(last_user_idx + 1)..]
|
|
964
|
-
|
|
965
|
-
last = current_turn.rfind { |m| m.role == :assistant && m.content && !m.content.to_s.empty? }
|
|
966
|
-
last&.content
|
|
967
|
-
end
|
|
968
|
-
|
|
969
|
-
def normalize_tool_calls(tool_calls)
|
|
970
|
-
return [] unless tool_calls
|
|
971
|
-
|
|
972
|
-
tool_calls.map do |tc|
|
|
973
|
-
if tc.is_a?(Hash)
|
|
974
|
-
ToolResultMessage.new(
|
|
975
|
-
tool: tc,
|
|
976
|
-
content: tc[:result] || tc['result']
|
|
977
|
-
)
|
|
978
|
-
else
|
|
979
|
-
tc
|
|
980
|
-
end
|
|
981
|
-
end
|
|
982
|
-
end
|
|
983
|
-
|
|
984
925
|
# Merge the stored on_content callback with a runtime streaming block.
|
|
985
926
|
# If both exist, both fire (stored first, then runtime block).
|
|
986
927
|
#
|
|
@@ -1124,29 +1065,5 @@ module RobotLab
|
|
|
1124
1065
|
RobotLab.config.logger.warn("[#{@name}] auto_compact: #{e.message}; skipping compaction")
|
|
1125
1066
|
end
|
|
1126
1067
|
end
|
|
1127
|
-
|
|
1128
|
-
# Install a per-run circuit breaker on the chat's on_tool_call hook.
|
|
1129
|
-
# Raises ToolLoopError if tool calls exceed @config.max_tool_rounds.
|
|
1130
|
-
# Stores the previous callback so restore_tool_call_callback can undo it.
|
|
1131
|
-
def install_circuit_breaker
|
|
1132
|
-
@circuit_breaker_call_count = 0
|
|
1133
|
-
max = @config.max_tool_rounds
|
|
1134
|
-
original = @on_tool_call
|
|
1135
|
-
|
|
1136
|
-
@chat.on_tool_call do |tool_call|
|
|
1137
|
-
@circuit_breaker_call_count += 1
|
|
1138
|
-
if @circuit_breaker_call_count > max
|
|
1139
|
-
raise ToolLoopError,
|
|
1140
|
-
"Circuit breaker triggered: #{@circuit_breaker_call_count} tool calls exceeded " \
|
|
1141
|
-
"max_tool_rounds (#{max})"
|
|
1142
|
-
end
|
|
1143
|
-
original&.call(tool_call)
|
|
1144
|
-
end
|
|
1145
|
-
end
|
|
1146
|
-
|
|
1147
|
-
# Restore the original on_tool_call callback after a circuit-breaker run.
|
|
1148
|
-
def restore_tool_call_callback
|
|
1149
|
-
@chat.on_tool_call(&@on_tool_call)
|
|
1150
|
-
end
|
|
1151
1068
|
end
|
|
1152
1069
|
end
|
data/lib/robot_lab/run_config.rb
CHANGED
|
@@ -12,20 +12,20 @@ module RobotLab
|
|
|
12
12
|
# config's non-nil values win over the less-specific config.
|
|
13
13
|
#
|
|
14
14
|
# @example Keyword construction
|
|
15
|
-
# config = RunConfig.new(model: "claude-sonnet-4", temperature: 0.7)
|
|
15
|
+
# config = RunConfig.new(model: "claude-sonnet-4-6", temperature: 0.7)
|
|
16
16
|
#
|
|
17
17
|
# @example Block DSL
|
|
18
18
|
# config = RunConfig.new do |c|
|
|
19
|
-
# c.model "claude-sonnet-4"
|
|
19
|
+
# c.model "claude-sonnet-4-6"
|
|
20
20
|
# c.temperature 0.7
|
|
21
21
|
# end
|
|
22
22
|
#
|
|
23
23
|
# @example Merge (more-specific wins)
|
|
24
|
-
# network_config = RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
|
|
24
|
+
# network_config = RunConfig.new(model: "claude-sonnet-4-6", temperature: 0.5)
|
|
25
25
|
# robot_config = RunConfig.new(temperature: 0.9)
|
|
26
26
|
# effective = network_config.merge(robot_config)
|
|
27
27
|
# effective.temperature #=> 0.9
|
|
28
|
-
# effective.model #=> "claude-sonnet-4"
|
|
28
|
+
# effective.model #=> "claude-sonnet-4-6"
|
|
29
29
|
#
|
|
30
30
|
# :reek:RepeatedConditional -- `if value` guards in independent field loops; each skips unset fields.
|
|
31
31
|
class RunConfig
|
|
@@ -106,6 +106,10 @@ module RobotLab
|
|
|
106
106
|
self.class.new(**merged)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# Fields that ruby_llm 2.0 accepts only through with_provider_options
|
|
110
|
+
# (merged into the request payload in the provider's own vocabulary).
|
|
111
|
+
PROVIDER_OPTION_FIELDS = %i[top_p top_k presence_penalty frequency_penalty stop].freeze
|
|
112
|
+
|
|
109
113
|
# Applies LLM fields to a chat object via its with_* methods.
|
|
110
114
|
#
|
|
111
115
|
# +provider+/+assume_model_exists+ are threaded through to +with_model+
|
|
@@ -117,27 +121,26 @@ module RobotLab
|
|
|
117
121
|
# raises ModelNotFoundError for any local-provider model (Ollama, etc.)
|
|
118
122
|
# not in RubyLLM's bundled registry.
|
|
119
123
|
#
|
|
124
|
+
# ruby_llm 2.0 mapping: +max_tokens+ applies via with_max_output_tokens,
|
|
125
|
+
# and the sampling knobs (top_p, top_k, penalties, stop) go through
|
|
126
|
+
# with_provider_options, merged with any options the chat already has.
|
|
127
|
+
#
|
|
120
128
|
# @param chat [Object] a RubyLLM::Chat (or similar) that responds to with_model, with_temperature, etc.
|
|
121
129
|
# @param provider [String, Symbol, nil] passed through to chat.with_model's provider: kwarg
|
|
122
|
-
# @param assume_model_exists [Boolean] passed through to chat.with_model's
|
|
130
|
+
# @param assume_model_exists [Boolean] passed through to chat.with_model's assume_model_exists: kwarg
|
|
123
131
|
# :reek:BooleanParameter -- assume_model_exists is a pass-through to RubyLLM's with_model kwarg.
|
|
124
132
|
# :reek:FeatureEnvy -- configuring the chat object handed in is exactly what apply_to is for.
|
|
133
|
+
# :reek:TooManyStatements -- one guarded application per LLM field group.
|
|
125
134
|
def apply_to(chat, provider: nil, assume_model_exists: false)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
chat.with_model(value, provider:, assume_exists: assume_model_exists) if chat.respond_to?(:with_model)
|
|
136
|
-
else
|
|
137
|
-
method = :"with_#{field}"
|
|
138
|
-
chat.public_send(method, value) if chat.respond_to?(method)
|
|
139
|
-
end
|
|
140
|
-
end
|
|
135
|
+
apply_model_to(chat, provider: provider, assume_model_exists: assume_model_exists)
|
|
136
|
+
|
|
137
|
+
temperature = @fields[:temperature]
|
|
138
|
+
chat.with_temperature(temperature) if temperature && chat.respond_to?(:with_temperature)
|
|
139
|
+
|
|
140
|
+
max_tokens = @fields[:max_tokens]
|
|
141
|
+
chat.with_max_output_tokens(max_tokens) if max_tokens && chat.respond_to?(:with_max_output_tokens)
|
|
142
|
+
|
|
143
|
+
apply_provider_options_to(chat)
|
|
141
144
|
end
|
|
142
145
|
|
|
143
146
|
# Build a RunConfig from prompt_manager front matter metadata.
|
|
@@ -187,6 +190,35 @@ module RobotLab
|
|
|
187
190
|
|
|
188
191
|
private
|
|
189
192
|
|
|
193
|
+
# Apply the model field, threading provider/assume_model_exists through
|
|
194
|
+
# only when a provider was actually given -- preserves the original
|
|
195
|
+
# single-arg call (and thus compatibility with any chat-like object
|
|
196
|
+
# exposing only `with_model(value)`) for the common case.
|
|
197
|
+
# :reek:BooleanParameter -- pass-through to RubyLLM's with_model kwarg.
|
|
198
|
+
# :reek:FeatureEnvy -- configuring the chat handed in is exactly what apply_to delegates here.
|
|
199
|
+
def apply_model_to(chat, provider:, assume_model_exists:)
|
|
200
|
+
model = @fields[:model]
|
|
201
|
+
return unless model && chat.respond_to?(:with_model)
|
|
202
|
+
|
|
203
|
+
if provider
|
|
204
|
+
chat.with_model(model, provider: provider, assume_model_exists: assume_model_exists)
|
|
205
|
+
else
|
|
206
|
+
chat.with_model(model)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Merge the sampling knobs into the chat's provider options.
|
|
211
|
+
# with_provider_options replaces prior options wholesale, so merge with
|
|
212
|
+
# the chat's current set to keep options applied earlier.
|
|
213
|
+
# :reek:FeatureEnvy -- configuring the chat handed in is exactly what apply_to delegates here.
|
|
214
|
+
def apply_provider_options_to(chat)
|
|
215
|
+
options = @fields.slice(*PROVIDER_OPTION_FIELDS)
|
|
216
|
+
return if options.empty? || !chat.respond_to?(:with_provider_options)
|
|
217
|
+
|
|
218
|
+
current = chat.respond_to?(:provider_options) ? chat.provider_options.to_h : {}
|
|
219
|
+
chat.with_provider_options(current.merge(options))
|
|
220
|
+
end
|
|
221
|
+
|
|
190
222
|
# Validates and stores a field value. Nil removes the key.
|
|
191
223
|
def set(field, value)
|
|
192
224
|
field = field.to_sym
|
data/lib/robot_lab/tool.rb
CHANGED
|
@@ -9,7 +9,7 @@ module RobotLab
|
|
|
9
9
|
#
|
|
10
10
|
# class GetWeather < RobotLab::Tool
|
|
11
11
|
# description "Get weather for a location"
|
|
12
|
-
#
|
|
12
|
+
# parameter :location, type: "string", description: "City name"
|
|
13
13
|
#
|
|
14
14
|
# def execute(location:)
|
|
15
15
|
# WeatherService.fetch(location)
|
|
@@ -93,10 +93,11 @@ module RobotLab
|
|
|
93
93
|
#
|
|
94
94
|
# For non-Ractor-safe tools: runs execute directly in the calling thread.
|
|
95
95
|
#
|
|
96
|
+
# @param tool_call [RubyLLM::ToolCall, nil] the originating tool call (supplied by the chat)
|
|
96
97
|
# @param args [Hash] the tool arguments from the LLM
|
|
97
98
|
# @return [Object] the tool result or an error string
|
|
98
99
|
# :reek:TooManyStatements -- hook-wrapped dispatch with per-error-class handling; the rescue clauses are the method.
|
|
99
|
-
def call(args)
|
|
100
|
+
def call(tool_call: nil, **args)
|
|
100
101
|
context = ToolCallHookContext.new(tool: self, tool_args: args, robot: @robot)
|
|
101
102
|
|
|
102
103
|
RobotLab::Hooks.run(:tool_call, context, **tool_hook_options) do
|
|
@@ -157,15 +158,9 @@ module RobotLab
|
|
|
157
158
|
tool_class = Class.new(self) do
|
|
158
159
|
description(desc_text) if desc_text
|
|
159
160
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
param pname.to_sym,
|
|
164
|
-
type: pdef[:type] || "string",
|
|
165
|
-
desc: pdef[:description],
|
|
166
|
-
required: required_list.include?(pname.to_s)
|
|
167
|
-
end
|
|
168
|
-
end
|
|
161
|
+
# ruby_llm 2.0's parameters() accepts a raw JSON Schema hash, so the
|
|
162
|
+
# full schema (including nested items/objects) survives verbatim.
|
|
163
|
+
parameters(params_hash) if params_hash.is_a?(Hash) && params_hash[:properties]
|
|
169
164
|
|
|
170
165
|
define_method(:execute) do |**args|
|
|
171
166
|
block.call(args)
|
|
@@ -183,7 +178,7 @@ module RobotLab
|
|
|
183
178
|
#
|
|
184
179
|
# @return [Hash] JSON Schema representation
|
|
185
180
|
def to_json_schema
|
|
186
|
-
schema =
|
|
181
|
+
schema = parameters_schema || { "type" => "object", "properties" => {}, "required" => [] }
|
|
187
182
|
{
|
|
188
183
|
name: name,
|
|
189
184
|
description: description,
|
data/lib/robot_lab/version.rb
CHANGED
data/lib/robot_lab.rb
CHANGED
|
@@ -39,7 +39,7 @@ require 'typed_bus'
|
|
|
39
39
|
# # See lib/robot_lab/config/defaults.yml for all options
|
|
40
40
|
#
|
|
41
41
|
# # Access configuration values:
|
|
42
|
-
# RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
|
|
42
|
+
# RobotLab.config.ruby_llm.model #=> "claude-sonnet-4-6"
|
|
43
43
|
# RobotLab.config.ruby_llm.request_timeout #=> 120
|
|
44
44
|
#
|
|
45
45
|
module RobotLab
|
|
@@ -159,7 +159,7 @@ module RobotLab
|
|
|
159
159
|
# @return [Config] the config instance
|
|
160
160
|
#
|
|
161
161
|
# @example
|
|
162
|
-
# RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
|
|
162
|
+
# RobotLab.config.ruby_llm.model #=> "claude-sonnet-4-6"
|
|
163
163
|
# RobotLab.config.ruby_llm.request_timeout #=> 120
|
|
164
164
|
# RobotLab.config.development? #=> true
|
|
165
165
|
def config
|
|
@@ -173,7 +173,7 @@ module RobotLab
|
|
|
173
173
|
#
|
|
174
174
|
# @example
|
|
175
175
|
# RobotLab.configure do |c|
|
|
176
|
-
# c.default_model = "claude-sonnet-4"
|
|
176
|
+
# c.default_model = "claude-sonnet-4-6"
|
|
177
177
|
# end
|
|
178
178
|
def configure
|
|
179
179
|
yield config
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robot_lab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -43,70 +43,56 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '1.12'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: ruby_llm-mcp
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '1.0'
|
|
46
|
+
version: '2.0'
|
|
61
47
|
type: :runtime
|
|
62
48
|
prerelease: false
|
|
63
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
50
|
requirements:
|
|
65
51
|
- - "~>"
|
|
66
52
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '
|
|
53
|
+
version: '2.0'
|
|
68
54
|
- !ruby/object:Gem::Dependency
|
|
69
|
-
name:
|
|
55
|
+
name: ruby_llm-providers-apfel
|
|
70
56
|
requirement: !ruby/object:Gem::Requirement
|
|
71
57
|
requirements:
|
|
72
58
|
- - "~>"
|
|
73
59
|
- !ruby/object:Gem::Version
|
|
74
|
-
version:
|
|
60
|
+
version: 0.2.1
|
|
75
61
|
type: :runtime
|
|
76
62
|
prerelease: false
|
|
77
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
64
|
requirements:
|
|
79
65
|
- - "~>"
|
|
80
66
|
- !ruby/object:Gem::Version
|
|
81
|
-
version:
|
|
67
|
+
version: 0.2.1
|
|
82
68
|
- !ruby/object:Gem::Dependency
|
|
83
|
-
name: ruby_llm-
|
|
69
|
+
name: ruby_llm-providers-lms
|
|
84
70
|
requirement: !ruby/object:Gem::Requirement
|
|
85
71
|
requirements:
|
|
86
72
|
- - "~>"
|
|
87
73
|
- !ruby/object:Gem::Version
|
|
88
|
-
version:
|
|
74
|
+
version: 0.2.1
|
|
89
75
|
type: :runtime
|
|
90
76
|
prerelease: false
|
|
91
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
78
|
requirements:
|
|
93
79
|
- - "~>"
|
|
94
80
|
- !ruby/object:Gem::Version
|
|
95
|
-
version:
|
|
81
|
+
version: 0.2.1
|
|
96
82
|
- !ruby/object:Gem::Dependency
|
|
97
|
-
name:
|
|
83
|
+
name: prompt_manager
|
|
98
84
|
requirement: !ruby/object:Gem::Requirement
|
|
99
85
|
requirements:
|
|
100
86
|
- - "~>"
|
|
101
87
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: '0
|
|
88
|
+
version: '1.0'
|
|
103
89
|
type: :runtime
|
|
104
90
|
prerelease: false
|
|
105
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
92
|
requirements:
|
|
107
93
|
- - "~>"
|
|
108
94
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: '0
|
|
95
|
+
version: '1.0'
|
|
110
96
|
- !ruby/object:Gem::Dependency
|
|
111
97
|
name: async
|
|
112
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -312,6 +298,7 @@ files:
|
|
|
312
298
|
- examples/14_rusty_circuit/scout.rb
|
|
313
299
|
- examples/14_rusty_circuit/show.log
|
|
314
300
|
- examples/15_memory_network_and_bus/.envrc
|
|
301
|
+
- examples/15_memory_network_and_bus/README.md
|
|
315
302
|
- examples/15_memory_network_and_bus/editor_in_chief.rb
|
|
316
303
|
- examples/15_memory_network_and_bus/editorial_pipeline.rb
|
|
317
304
|
- examples/15_memory_network_and_bus/linux_writer.rb
|
|
@@ -324,6 +311,7 @@ files:
|
|
|
324
311
|
- examples/15_memory_network_and_bus/output/memory.json
|
|
325
312
|
- examples/15_memory_network_and_bus/output/revision_1.md
|
|
326
313
|
- examples/15_memory_network_and_bus/output/revision_2.md
|
|
314
|
+
- examples/15_memory_network_and_bus/output/revision_3.md
|
|
327
315
|
- examples/15_memory_network_and_bus/output/windows_draft.md
|
|
328
316
|
- examples/15_memory_network_and_bus/prompts/os_advocate.md
|
|
329
317
|
- examples/15_memory_network_and_bus/prompts/os_chief.md
|
|
@@ -387,6 +375,7 @@ files:
|
|
|
387
375
|
- examples/26_document_store/postgres_runbook.md
|
|
388
376
|
- examples/26_document_store/redis_caching_guide.md
|
|
389
377
|
- examples/26_document_store/sidekiq_guide.md
|
|
378
|
+
- examples/27_incident_response/README.md
|
|
390
379
|
- examples/27_incident_response/incident_response.rb
|
|
391
380
|
- examples/28_mcp_discovery.rb
|
|
392
381
|
- examples/29_ractor_tools.rb
|
|
@@ -448,6 +437,7 @@ files:
|
|
|
448
437
|
- examples/prompts/template_with_skills_test.md
|
|
449
438
|
- examples/prompts/triage.md
|
|
450
439
|
- examples/ruboruby.md
|
|
440
|
+
- examples/run_all.rb
|
|
451
441
|
- examples/xyzzy.rb
|
|
452
442
|
- lib/robot_lab.rb
|
|
453
443
|
- lib/robot_lab/agent_skill.rb
|
|
@@ -490,6 +480,7 @@ files:
|
|
|
490
480
|
- lib/robot_lab/robot/history_search.rb
|
|
491
481
|
- lib/robot_lab/robot/hooking.rb
|
|
492
482
|
- lib/robot_lab/robot/mcp_management.rb
|
|
483
|
+
- lib/robot_lab/robot/result_building.rb
|
|
493
484
|
- lib/robot_lab/robot/template_rendering.rb
|
|
494
485
|
- lib/robot_lab/robot_message.rb
|
|
495
486
|
- lib/robot_lab/robot_result.rb
|
|
@@ -541,7 +532,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
541
532
|
- !ruby/object:Gem::Version
|
|
542
533
|
version: '0'
|
|
543
534
|
requirements: []
|
|
544
|
-
rubygems_version: 4.0.
|
|
535
|
+
rubygems_version: 4.0.21
|
|
545
536
|
specification_version: 4
|
|
546
537
|
summary: Ruby framework for building and orchestrating multi-robot LLM workflows
|
|
547
538
|
test_files: []
|