robot_lab 0.0.1 → 0.0.4
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/.github/workflows/deploy-github-pages.yml +9 -9
- data/.irbrc +6 -0
- data/CHANGELOG.md +90 -0
- data/README.md +203 -46
- data/Rakefile +70 -1
- data/docs/api/core/index.md +12 -0
- data/docs/api/core/robot.md +478 -130
- data/docs/api/core/tool.md +205 -209
- data/docs/api/history/active-record-adapter.md +174 -94
- data/docs/api/history/config.md +186 -93
- data/docs/api/history/index.md +57 -61
- data/docs/api/history/thread-manager.md +123 -73
- data/docs/api/mcp/client.md +119 -48
- data/docs/api/mcp/index.md +75 -60
- data/docs/api/mcp/server.md +120 -136
- data/docs/api/mcp/transports.md +172 -184
- data/docs/api/streaming/context.md +157 -74
- data/docs/api/streaming/events.md +114 -166
- data/docs/api/streaming/index.md +74 -72
- data/docs/architecture/core-concepts.md +361 -112
- data/docs/architecture/index.md +97 -59
- data/docs/architecture/message-flow.md +138 -129
- data/docs/architecture/network-orchestration.md +197 -50
- data/docs/architecture/robot-execution.md +199 -146
- data/docs/architecture/state-management.md +255 -187
- data/docs/concepts.md +312 -48
- data/docs/examples/basic-chat.md +89 -77
- data/docs/examples/index.md +222 -47
- data/docs/examples/mcp-server.md +207 -203
- data/docs/examples/multi-robot-network.md +129 -35
- data/docs/examples/rails-application.md +159 -160
- data/docs/examples/tool-usage.md +295 -204
- data/docs/getting-started/configuration.md +275 -162
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/installation.md +22 -13
- data/docs/getting-started/quick-start.md +166 -121
- data/docs/guides/building-robots.md +417 -212
- data/docs/guides/creating-networks.md +94 -24
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/streaming.md +80 -110
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +50 -37
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +13 -14
- data/examples/04_mcp.rb +5 -8
- data/examples/05_streaming.rb +5 -8
- data/examples/06_prompt_templates.rb +42 -37
- data/examples/07_network_memory.rb +13 -14
- data/examples/08_llm_config.rb +140 -0
- data/examples/09_chaining.rb +223 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +230 -0
- data/examples/12_message_bus.rb +74 -0
- data/examples/13_spawn.rb +90 -0
- data/examples/14_rusty_circuit/comic.rb +143 -0
- data/examples/14_rusty_circuit/display.rb +203 -0
- data/examples/14_rusty_circuit/heckler.rb +57 -0
- data/examples/14_rusty_circuit/open_mic.rb +121 -0
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
- data/examples/14_rusty_circuit/scout.rb +173 -0
- data/examples/14_rusty_circuit/scout_notes.md +89 -0
- data/examples/14_rusty_circuit/show.log +234 -0
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
- data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
- data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
- data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
- data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
- data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
- data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
- data/examples/15_memory_network_and_bus/output/memory.json +13 -0
- data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
- data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
- data/examples/README.md +197 -0
- data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
- data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
- data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
- data/examples/prompts/comedian.md +6 -0
- data/examples/prompts/comedy_critic.md +10 -0
- data/examples/prompts/configurable.md +9 -0
- data/examples/prompts/dispatcher.md +12 -0
- data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
- data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
- data/examples/prompts/frontmatter_mcp_test.md +9 -0
- data/examples/prompts/frontmatter_named_test.md +5 -0
- data/examples/prompts/frontmatter_tools_test.md +6 -0
- data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
- data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
- data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
- data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
- data/examples/prompts/llm_config_demo.md +20 -0
- data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
- data/examples/prompts/os_advocate.md +13 -0
- data/examples/prompts/os_chief.md +13 -0
- data/examples/prompts/os_editor.md +13 -0
- data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
- data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
- data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
- data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
- data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +1 -1
- data/lib/robot_lab/adapters/openai.rb +2 -1
- data/lib/robot_lab/ask_user.rb +75 -0
- data/lib/robot_lab/config/defaults.yml +121 -0
- data/lib/robot_lab/config.rb +183 -0
- data/lib/robot_lab/error.rb +6 -0
- data/lib/robot_lab/mcp/client.rb +1 -1
- data/lib/robot_lab/memory.rb +2 -2
- data/lib/robot_lab/robot.rb +523 -249
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/robotic_model.rb +1 -1
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/tool.rb +108 -172
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +2 -18
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +66 -55
- metadata +107 -116
- data/examples/prompts/assistant/user.txt.erb +0 -1
- data/examples/prompts/billing/user.txt.erb +0 -1
- data/examples/prompts/classifier/user.txt.erb +0 -1
- data/examples/prompts/entity_extractor/user.txt.erb +0 -3
- data/examples/prompts/escalation/user.txt.erb +0 -34
- data/examples/prompts/general/user.txt.erb +0 -1
- data/examples/prompts/github_assistant/user.txt.erb +0 -1
- data/examples/prompts/helper/user.txt.erb +0 -1
- data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
- data/examples/prompts/order_support/user.txt.erb +0 -22
- data/examples/prompts/product_support/user.txt.erb +0 -32
- data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
- data/examples/prompts/synthesizer/user.txt.erb +0 -15
- data/examples/prompts/technical/user.txt.erb +0 -1
- data/examples/prompts/triage/user.txt.erb +0 -17
- data/lib/robot_lab/configuration.rb +0 -143
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example 8: LLM Configuration via MywayConfig
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates how RobotLab uses MywayConfig for environment-specific
|
|
7
|
+
# configuration (similar to Rails database.yml).
|
|
8
|
+
#
|
|
9
|
+
# Configuration is loaded from multiple sources in priority order:
|
|
10
|
+
# 1. Bundled defaults (lib/robot_lab/config/defaults.yml)
|
|
11
|
+
# 2. Environment-specific overrides (development, test, production)
|
|
12
|
+
# 3. XDG user config (~/.config/robot_lab/config.yml)
|
|
13
|
+
# 4. Project config (./config/robot_lab.yml)
|
|
14
|
+
# 5. Environment variables (ROBOT_LAB_*)
|
|
15
|
+
# 6. Template front matter (model, temperature, etc. in .md YAML header)
|
|
16
|
+
# 7. Constructor parameters (model:, temperature:, etc. passed to Robot.new)
|
|
17
|
+
# 8. with_* methods (runtime chaining, e.g. robot.with_temperature(0.9))
|
|
18
|
+
# 9. Run-time context (kwargs passed to robot.run that re-render the template)
|
|
19
|
+
#
|
|
20
|
+
# Environment is determined by ROBOT_LAB_ENV, RAILS_ENV, or RACK_ENV.
|
|
21
|
+
#
|
|
22
|
+
# Environment variable examples:
|
|
23
|
+
# ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
|
|
24
|
+
# ROBOT_LAB_RUBY_LLM__MODEL=gpt-4
|
|
25
|
+
# ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180
|
|
26
|
+
#
|
|
27
|
+
# Usage:
|
|
28
|
+
# ANTHROPIC_API_KEY=your_key ruby examples/08_llm_config.rb
|
|
29
|
+
# ROBOT_LAB_ENV=test ANTHROPIC_API_KEY=your_key ruby examples/08_llm_config.rb
|
|
30
|
+
# ROBOT_LAB_ENV=production ANTHROPIC_API_KEY=your_key ruby examples/08_llm_config.rb
|
|
31
|
+
|
|
32
|
+
# Configure template path before loading (MywayConfig reads env vars on init)
|
|
33
|
+
ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
|
|
34
|
+
|
|
35
|
+
require_relative "../lib/robot_lab"
|
|
36
|
+
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# Demonstration
|
|
39
|
+
# =============================================================================
|
|
40
|
+
|
|
41
|
+
puts "=" * 70
|
|
42
|
+
puts "Example 8: LLM Configuration via MywayConfig"
|
|
43
|
+
puts "=" * 70
|
|
44
|
+
puts
|
|
45
|
+
|
|
46
|
+
# --- Gather environment from the user via AskUser ---
|
|
47
|
+
ask = RobotLab::AskUser.new
|
|
48
|
+
env = ask.call(
|
|
49
|
+
"question" => "Which environment should the demo run in?",
|
|
50
|
+
"choices" => %w[development test production],
|
|
51
|
+
"default" => ENV['ROBOT_LAB_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development"
|
|
52
|
+
)
|
|
53
|
+
puts
|
|
54
|
+
|
|
55
|
+
# Display configuration values (hiding sensitive keys)
|
|
56
|
+
config = RobotLab.config
|
|
57
|
+
|
|
58
|
+
puts "Core Settings:"
|
|
59
|
+
puts " max_iterations: #{config.max_iterations}"
|
|
60
|
+
puts " streaming_enabled: #{config.streaming_enabled}"
|
|
61
|
+
puts " template_path: #{config.template_path || '(default)'}"
|
|
62
|
+
puts
|
|
63
|
+
|
|
64
|
+
puts "RubyLLM Settings:"
|
|
65
|
+
puts " model: #{config.ruby_llm.model}"
|
|
66
|
+
puts " provider: #{config.ruby_llm.provider}"
|
|
67
|
+
puts " request_timeout: #{config.ruby_llm.request_timeout}s"
|
|
68
|
+
puts " max_retries: #{config.ruby_llm.max_retries}"
|
|
69
|
+
puts " log_level: #{config.ruby_llm.log_level}"
|
|
70
|
+
|
|
71
|
+
# Show API key status without revealing values
|
|
72
|
+
api_key = config.ruby_llm.anthropic_api_key
|
|
73
|
+
puts " anthropic_api_key: #{api_key ? '[SET via config]' : (ENV['ANTHROPIC_API_KEY'] ? '[SET via env]' : '(not set)')}"
|
|
74
|
+
puts
|
|
75
|
+
|
|
76
|
+
puts "-" * 70
|
|
77
|
+
puts "Configuration hierarchy (highest priority first):"
|
|
78
|
+
puts
|
|
79
|
+
puts " Per-Robot (override global settings for a specific robot):"
|
|
80
|
+
puts " 9. Run-time context: kwargs to robot.run re-render template"
|
|
81
|
+
puts " 8. with_* methods: robot.with_temperature(0.9).ask(...)"
|
|
82
|
+
puts " 7. Constructor params: Robot.new(model: ..., temperature: ...)"
|
|
83
|
+
puts " 6. Template front matter: model, temperature, etc. in .md YAML header"
|
|
84
|
+
puts
|
|
85
|
+
puts " Global (apply to all robots unless overridden):"
|
|
86
|
+
puts " 5. Environment variables: ROBOT_LAB_RUBY_LLM__MODEL, etc."
|
|
87
|
+
puts " 4. Project config: ./config/robot_lab.yml"
|
|
88
|
+
puts " 3. User config: ~/.config/robot_lab/config.yml"
|
|
89
|
+
puts " 2. Environment overrides: #{env} section in defaults.yml"
|
|
90
|
+
puts " 1. Bundled defaults: lib/robot_lab/config/defaults.yml"
|
|
91
|
+
puts "-" * 70
|
|
92
|
+
puts
|
|
93
|
+
|
|
94
|
+
# Create a robot using the configuration
|
|
95
|
+
robot = RobotLab.build(
|
|
96
|
+
name: "config_demo",
|
|
97
|
+
template: :llm_config_demo,
|
|
98
|
+
local_tools: [RobotLab::AskUser],
|
|
99
|
+
context: {
|
|
100
|
+
environment: env,
|
|
101
|
+
model: config.ruby_llm.model,
|
|
102
|
+
provider: config.ruby_llm.provider.to_s
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
puts "Running robot with #{env} configuration..."
|
|
107
|
+
puts "Model: #{robot.model}"
|
|
108
|
+
puts
|
|
109
|
+
|
|
110
|
+
# Run the robot
|
|
111
|
+
result = robot.run(
|
|
112
|
+
"Briefly explain how environment-specific LLM configuration works " \
|
|
113
|
+
"and why it's useful."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Display the result
|
|
117
|
+
puts "Response:"
|
|
118
|
+
result.output.each do |message|
|
|
119
|
+
puts message.content if message.respond_to?(:content)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
puts <<~FOOTER
|
|
123
|
+
|
|
124
|
+
#{"=" * 70}
|
|
125
|
+
Configuration demonstrated successfully!
|
|
126
|
+
|
|
127
|
+
Configuration flows through two layers:
|
|
128
|
+
Global (MywayConfig): YAML defaults, env overrides, XDG, env vars
|
|
129
|
+
Per-Robot: template front matter, constructor params, with_* methods
|
|
130
|
+
|
|
131
|
+
Example environment variable overrides:
|
|
132
|
+
ROBOT_LAB_RUBY_LLM__MODEL=gpt-4
|
|
133
|
+
ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180
|
|
134
|
+
ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
|
|
135
|
+
|
|
136
|
+
Try running with different environments:
|
|
137
|
+
ROBOT_LAB_ENV=test ruby examples/08_llm_config.rb
|
|
138
|
+
ROBOT_LAB_ENV=production ruby examples/08_llm_config.rb
|
|
139
|
+
#{"=" * 70}
|
|
140
|
+
FOOTER
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example 9: Robot Chaining & Reconfiguration
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates the Robot API surface for runtime configuration without
|
|
7
|
+
# making any LLM calls. Covers:
|
|
8
|
+
# - with_* method chaining
|
|
9
|
+
# - update() for reconfiguration
|
|
10
|
+
# - to_h introspection (via amazing_print)
|
|
11
|
+
# - Config diffs between steps (via hashdiff)
|
|
12
|
+
# - Template front matter config keys
|
|
13
|
+
# - Constructor params overriding front matter
|
|
14
|
+
# - Config hierarchy in action
|
|
15
|
+
# - AskUser tool for gathering template parameters interactively
|
|
16
|
+
#
|
|
17
|
+
# Usage:
|
|
18
|
+
# bundle exec ruby examples/09_chaining.rb
|
|
19
|
+
|
|
20
|
+
# Configure template path before loading
|
|
21
|
+
ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
|
|
22
|
+
|
|
23
|
+
require_relative "../lib/robot_lab"
|
|
24
|
+
require "amazing_print"
|
|
25
|
+
require "hashdiff"
|
|
26
|
+
require "stringio"
|
|
27
|
+
|
|
28
|
+
# Show a config snapshot and the diff from the previous snapshot.
|
|
29
|
+
# Returns the current hash for use as the next "previous" snapshot.
|
|
30
|
+
def show_config(robot, previous_config = nil)
|
|
31
|
+
current = robot.to_h
|
|
32
|
+
ap current
|
|
33
|
+
|
|
34
|
+
if previous_config
|
|
35
|
+
diff = Hashdiff.diff(previous_config, current)
|
|
36
|
+
if diff.empty?
|
|
37
|
+
puts " (no changes to to_h)"
|
|
38
|
+
else
|
|
39
|
+
puts
|
|
40
|
+
puts " Diff from previous:"
|
|
41
|
+
diff.each do |change|
|
|
42
|
+
op, path, *values = change
|
|
43
|
+
case op
|
|
44
|
+
when "+"
|
|
45
|
+
puts " + #{path}: #{values.first.inspect}"
|
|
46
|
+
when "-"
|
|
47
|
+
puts " - #{path}: #{values.first.inspect}"
|
|
48
|
+
when "~"
|
|
49
|
+
puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
current
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts "=" * 70
|
|
59
|
+
puts "Example 9: Robot Chaining & Reconfiguration"
|
|
60
|
+
puts "=" * 70
|
|
61
|
+
puts
|
|
62
|
+
|
|
63
|
+
# =============================================================================
|
|
64
|
+
# Section 1: Template front matter config
|
|
65
|
+
# =============================================================================
|
|
66
|
+
|
|
67
|
+
puts "--- Section 1: Template Front Matter Config ---"
|
|
68
|
+
puts
|
|
69
|
+
|
|
70
|
+
# The 'configurable' template sets temperature: 0.3 and max_tokens: 200
|
|
71
|
+
# via YAML front matter. These are applied automatically.
|
|
72
|
+
robot = RobotLab.build(
|
|
73
|
+
name: "chameleon",
|
|
74
|
+
template: :configurable,
|
|
75
|
+
context: { task_type: "analysis" }
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
puts "Robot created with :configurable template"
|
|
79
|
+
prev = show_config(robot)
|
|
80
|
+
puts
|
|
81
|
+
|
|
82
|
+
# =============================================================================
|
|
83
|
+
# Section 2: with_* method chaining
|
|
84
|
+
# =============================================================================
|
|
85
|
+
|
|
86
|
+
puts "--- Section 2: with_* Method Chaining ---"
|
|
87
|
+
puts
|
|
88
|
+
|
|
89
|
+
# with_* methods return self, enabling fluent chaining.
|
|
90
|
+
# These override whatever the template set.
|
|
91
|
+
robot.with_temperature(0.9)
|
|
92
|
+
|
|
93
|
+
puts "After chaining with_temperature(0.9):"
|
|
94
|
+
prev = show_config(robot, prev)
|
|
95
|
+
puts
|
|
96
|
+
|
|
97
|
+
# =============================================================================
|
|
98
|
+
# Section 3: update() for reconfiguration
|
|
99
|
+
# =============================================================================
|
|
100
|
+
|
|
101
|
+
puts "--- Section 3: update() for Reconfiguration ---"
|
|
102
|
+
puts
|
|
103
|
+
|
|
104
|
+
# update() can swap the template, model, temperature, and other settings.
|
|
105
|
+
robot.update(template: :assistant, temperature: 0.5)
|
|
106
|
+
|
|
107
|
+
puts "After update(template: :assistant, temperature: 0.5):"
|
|
108
|
+
prev = show_config(robot, prev)
|
|
109
|
+
puts
|
|
110
|
+
|
|
111
|
+
# Swap back with context
|
|
112
|
+
robot.update(template: :configurable, context: { task_type: "creative" })
|
|
113
|
+
|
|
114
|
+
puts "After update(template: :configurable, context: { task_type: 'creative' }):"
|
|
115
|
+
prev = show_config(robot, prev)
|
|
116
|
+
puts
|
|
117
|
+
|
|
118
|
+
# =============================================================================
|
|
119
|
+
# Section 4: Constructor params override front matter
|
|
120
|
+
# =============================================================================
|
|
121
|
+
|
|
122
|
+
puts "--- Section 4: Constructor Params Override Front Matter ---"
|
|
123
|
+
puts
|
|
124
|
+
|
|
125
|
+
# The configurable template sets temperature: 0.3 in front matter.
|
|
126
|
+
# Passing temperature: 0.9 to the constructor overrides it.
|
|
127
|
+
robot2 = RobotLab.build(
|
|
128
|
+
name: "override_demo",
|
|
129
|
+
template: :configurable,
|
|
130
|
+
context: { task_type: "creative" },
|
|
131
|
+
temperature: 0.9
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
puts "Template sets temperature: 0.3, constructor passes temperature: 0.9"
|
|
135
|
+
prev2 = show_config(robot2)
|
|
136
|
+
puts
|
|
137
|
+
|
|
138
|
+
# =============================================================================
|
|
139
|
+
# Section 5: Bare robot with chaining
|
|
140
|
+
# =============================================================================
|
|
141
|
+
|
|
142
|
+
puts "--- Section 5: Bare Robot with Chaining ---"
|
|
143
|
+
puts
|
|
144
|
+
|
|
145
|
+
# A bare robot has no template. Configure entirely via chaining.
|
|
146
|
+
bare = RobotLab.build(name: "bare")
|
|
147
|
+
|
|
148
|
+
puts "Bare robot before chaining:"
|
|
149
|
+
prev_bare = show_config(bare)
|
|
150
|
+
puts
|
|
151
|
+
|
|
152
|
+
bare
|
|
153
|
+
.with_instructions("You are a helpful assistant.")
|
|
154
|
+
.with_temperature(0.5)
|
|
155
|
+
|
|
156
|
+
puts "After with_instructions(...).with_temperature(0.5):"
|
|
157
|
+
prev_bare = show_config(bare, prev_bare)
|
|
158
|
+
puts
|
|
159
|
+
|
|
160
|
+
# =============================================================================
|
|
161
|
+
# Section 6: with_template() on an existing robot
|
|
162
|
+
# =============================================================================
|
|
163
|
+
|
|
164
|
+
puts "--- Section 6: with_template() on Existing Robot ---"
|
|
165
|
+
puts
|
|
166
|
+
|
|
167
|
+
# You can apply a template to a robot after creation.
|
|
168
|
+
bare.with_template(:helper)
|
|
169
|
+
|
|
170
|
+
puts "After bare.with_template(:helper):"
|
|
171
|
+
show_config(bare, prev_bare)
|
|
172
|
+
puts
|
|
173
|
+
|
|
174
|
+
# =============================================================================
|
|
175
|
+
# Section 7: AskUser tool for gathering template parameters
|
|
176
|
+
# =============================================================================
|
|
177
|
+
|
|
178
|
+
puts "--- Section 7: AskUser for Template Parameters ---"
|
|
179
|
+
puts
|
|
180
|
+
puts "The :configurable template declares `task_type: general` in its front"
|
|
181
|
+
puts "matter. That default is offered to the user — they can accept it by"
|
|
182
|
+
puts "pressing Enter or type something else. Parameters with null values"
|
|
183
|
+
puts "have no default and require user input."
|
|
184
|
+
puts
|
|
185
|
+
|
|
186
|
+
# Build a robot with AskUser — the template's task_type default ("general")
|
|
187
|
+
# is offered to the user, who can accept or override it.
|
|
188
|
+
interactive = RobotLab.build(
|
|
189
|
+
name: "interactive_demo",
|
|
190
|
+
template: :configurable,
|
|
191
|
+
local_tools: [RobotLab::AskUser]
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
puts "Robot 'interactive_demo' has AskUser in its tools."
|
|
195
|
+
puts "The template renders with the default: task_type = \"general\""
|
|
196
|
+
puts "When run, the robot can call ask_user to let the user confirm"
|
|
197
|
+
puts "or change the value."
|
|
198
|
+
puts
|
|
199
|
+
|
|
200
|
+
# Simulate what the user would see (no LLM call needed)
|
|
201
|
+
output = StringIO.new
|
|
202
|
+
demo_tool = RobotLab::AskUser.new(robot: interactive)
|
|
203
|
+
interactive.output = output
|
|
204
|
+
interactive.input = StringIO.new("2\n")
|
|
205
|
+
|
|
206
|
+
result = demo_tool.call(
|
|
207
|
+
"question" => "What type of task should I optimize for?",
|
|
208
|
+
"choices" => %w[general analysis creative coding research],
|
|
209
|
+
"default" => "general"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
puts output.string
|
|
213
|
+
puts "User selected: #{result}"
|
|
214
|
+
puts
|
|
215
|
+
puts "The robot would re-render the template with task_type: \"#{result}\""
|
|
216
|
+
puts "producing: \"You are a precise assistant optimized for #{result} tasks.\""
|
|
217
|
+
puts
|
|
218
|
+
puts "If the user had pressed Enter, the default \"general\" would be kept."
|
|
219
|
+
puts
|
|
220
|
+
|
|
221
|
+
puts "=" * 70
|
|
222
|
+
puts "All sections completed without any LLM calls."
|
|
223
|
+
puts "=" * 70
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example 10: Advanced Memory Operations
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates the Memory API without making any LLM calls. Covers:
|
|
7
|
+
# - StateProxy for method-style data access
|
|
8
|
+
# - Key subscriptions with MemoryChange objects
|
|
9
|
+
# - Pattern subscriptions
|
|
10
|
+
# - Unsubscribe
|
|
11
|
+
# - Key enumeration (keys, all_keys, key?)
|
|
12
|
+
# - Serialization (to_h, from_hash, to_json) with amazing_print
|
|
13
|
+
# - Serialization round-trip verification with hashdiff
|
|
14
|
+
# - Clone for isolated copies
|
|
15
|
+
# - Delete with reserved key protection
|
|
16
|
+
# - Clear vs reset
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# bundle exec ruby examples/10_memory.rb
|
|
20
|
+
|
|
21
|
+
# Configure template path before loading
|
|
22
|
+
ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
|
|
23
|
+
|
|
24
|
+
require_relative "../lib/robot_lab"
|
|
25
|
+
require "json"
|
|
26
|
+
require "amazing_print"
|
|
27
|
+
require "hashdiff"
|
|
28
|
+
|
|
29
|
+
puts "=" * 70
|
|
30
|
+
puts "Example 10: Advanced Memory Operations"
|
|
31
|
+
puts "=" * 70
|
|
32
|
+
puts
|
|
33
|
+
|
|
34
|
+
# =============================================================================
|
|
35
|
+
# Section 1: StateProxy for method-style access
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
puts "--- Section 1: StateProxy for Method-Style Access ---"
|
|
39
|
+
puts
|
|
40
|
+
|
|
41
|
+
memory = RobotLab.create_memory(
|
|
42
|
+
data: { category: nil, priority: "low" },
|
|
43
|
+
enable_cache: false
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
puts "Initial data:"
|
|
47
|
+
ap memory.data.to_h
|
|
48
|
+
puts
|
|
49
|
+
|
|
50
|
+
# StateProxy allows method-style access to the :data hash
|
|
51
|
+
memory.data.category = "billing"
|
|
52
|
+
|
|
53
|
+
puts "After memory.data.category = 'billing':"
|
|
54
|
+
ap memory.data.to_h
|
|
55
|
+
puts
|
|
56
|
+
|
|
57
|
+
# Bracket-style also works
|
|
58
|
+
memory.data[:priority] = "high"
|
|
59
|
+
puts "After memory.data[:priority] = 'high':"
|
|
60
|
+
ap memory.data.to_h
|
|
61
|
+
puts
|
|
62
|
+
|
|
63
|
+
# =============================================================================
|
|
64
|
+
# Section 2: Subscriptions and MemoryChange
|
|
65
|
+
# =============================================================================
|
|
66
|
+
|
|
67
|
+
puts "--- Section 2: Subscriptions and MemoryChange ---"
|
|
68
|
+
puts
|
|
69
|
+
|
|
70
|
+
changes = []
|
|
71
|
+
|
|
72
|
+
sub_id = memory.subscribe(:status) do |change|
|
|
73
|
+
changes << change
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
puts "Subscribed to :status (sub_id: #{sub_id[0..7]}...)"
|
|
77
|
+
|
|
78
|
+
# Set current_writer so the MemoryChange knows who wrote
|
|
79
|
+
memory.current_writer = "classifier"
|
|
80
|
+
memory.set(:status, "processing")
|
|
81
|
+
|
|
82
|
+
# Give the async callback time to fire
|
|
83
|
+
sleep 0.1
|
|
84
|
+
|
|
85
|
+
if changes.any?
|
|
86
|
+
change = changes.first
|
|
87
|
+
puts "Change received:"
|
|
88
|
+
ap change.to_h
|
|
89
|
+
puts " created?: #{change.created?}"
|
|
90
|
+
puts " updated?: #{change.updated?}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Update the same key to see an 'updated' change
|
|
94
|
+
memory.set(:status, "complete")
|
|
95
|
+
sleep 0.1
|
|
96
|
+
|
|
97
|
+
if changes.size > 1
|
|
98
|
+
puts
|
|
99
|
+
puts "Second change (update):"
|
|
100
|
+
ap changes.last.to_h
|
|
101
|
+
puts " created?: #{changes.last.created?}"
|
|
102
|
+
puts " updated?: #{changes.last.updated?}"
|
|
103
|
+
end
|
|
104
|
+
puts
|
|
105
|
+
|
|
106
|
+
# =============================================================================
|
|
107
|
+
# Section 3: Pattern subscriptions
|
|
108
|
+
# =============================================================================
|
|
109
|
+
|
|
110
|
+
puts "--- Section 3: Pattern Subscriptions ---"
|
|
111
|
+
puts
|
|
112
|
+
|
|
113
|
+
pattern_changes = []
|
|
114
|
+
|
|
115
|
+
pattern_sub_id = memory.subscribe_pattern("analysis:*") do |change|
|
|
116
|
+
pattern_changes << change
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
puts "Subscribed to pattern 'analysis:*'"
|
|
120
|
+
|
|
121
|
+
memory.current_writer = "analyst"
|
|
122
|
+
memory.set(:"analysis:sentiment", { score: 0.8 })
|
|
123
|
+
memory.set(:"analysis:entities", ["Ruby", "LLM"])
|
|
124
|
+
memory.set(:unrelated_key, "ignored")
|
|
125
|
+
|
|
126
|
+
sleep 0.1
|
|
127
|
+
|
|
128
|
+
puts "Pattern matched #{pattern_changes.size} changes (expected 2):"
|
|
129
|
+
pattern_changes.each do |change|
|
|
130
|
+
ap({ change.key => change.value })
|
|
131
|
+
end
|
|
132
|
+
puts
|
|
133
|
+
|
|
134
|
+
# =============================================================================
|
|
135
|
+
# Section 4: Unsubscribe
|
|
136
|
+
# =============================================================================
|
|
137
|
+
|
|
138
|
+
puts "--- Section 4: Unsubscribe ---"
|
|
139
|
+
puts
|
|
140
|
+
|
|
141
|
+
count_before = changes.size
|
|
142
|
+
memory.unsubscribe(sub_id)
|
|
143
|
+
puts "Unsubscribed from :status"
|
|
144
|
+
|
|
145
|
+
memory.set(:status, "archived")
|
|
146
|
+
sleep 0.1
|
|
147
|
+
|
|
148
|
+
puts "Changes after unsubscribe: #{changes.size} (was #{count_before}, should be same)"
|
|
149
|
+
puts
|
|
150
|
+
|
|
151
|
+
# =============================================================================
|
|
152
|
+
# Section 5: Key management
|
|
153
|
+
# =============================================================================
|
|
154
|
+
|
|
155
|
+
puts "--- Section 5: Key Management ---"
|
|
156
|
+
puts
|
|
157
|
+
|
|
158
|
+
puts "memory.keys (non-reserved):"
|
|
159
|
+
ap memory.keys
|
|
160
|
+
puts
|
|
161
|
+
|
|
162
|
+
puts "memory.all_keys (includes reserved: data, results, messages, session_id, cache):"
|
|
163
|
+
ap memory.all_keys
|
|
164
|
+
puts
|
|
165
|
+
|
|
166
|
+
puts "memory.key?(:status) = #{memory.key?(:status)}"
|
|
167
|
+
puts "memory.key?(:nope) = #{memory.key?(:nope)}"
|
|
168
|
+
puts
|
|
169
|
+
|
|
170
|
+
# =============================================================================
|
|
171
|
+
# Section 6: Serialization round-trip
|
|
172
|
+
# =============================================================================
|
|
173
|
+
|
|
174
|
+
puts "--- Section 6: Serialization Round-Trip ---"
|
|
175
|
+
puts
|
|
176
|
+
|
|
177
|
+
hash = memory.to_h
|
|
178
|
+
puts "memory.to_h:"
|
|
179
|
+
ap hash
|
|
180
|
+
puts
|
|
181
|
+
|
|
182
|
+
json_str = memory.to_json
|
|
183
|
+
puts "memory.to_json length: #{json_str.length} chars"
|
|
184
|
+
puts
|
|
185
|
+
|
|
186
|
+
# Round-trip via from_hash
|
|
187
|
+
restored = RobotLab::Memory.from_hash(hash)
|
|
188
|
+
|
|
189
|
+
puts "Restored memory from hash:"
|
|
190
|
+
ap restored.to_h
|
|
191
|
+
puts
|
|
192
|
+
|
|
193
|
+
# Use hashdiff to verify the round-trip preserved everything
|
|
194
|
+
diff = Hashdiff.diff(hash, restored.to_h)
|
|
195
|
+
if diff.empty?
|
|
196
|
+
puts "Round-trip verification: PERFECT (no differences)"
|
|
197
|
+
else
|
|
198
|
+
puts "Round-trip differences:"
|
|
199
|
+
diff.each do |change|
|
|
200
|
+
op, path, *values = change
|
|
201
|
+
case op
|
|
202
|
+
when "+"
|
|
203
|
+
puts " + #{path}: #{values.first.inspect}"
|
|
204
|
+
when "-"
|
|
205
|
+
puts " - #{path}: #{values.first.inspect}"
|
|
206
|
+
when "~"
|
|
207
|
+
puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
puts
|
|
212
|
+
|
|
213
|
+
# =============================================================================
|
|
214
|
+
# Section 7: Clone for isolation
|
|
215
|
+
# =============================================================================
|
|
216
|
+
|
|
217
|
+
puts "--- Section 7: Clone for Isolation ---"
|
|
218
|
+
puts
|
|
219
|
+
|
|
220
|
+
cloned = memory.clone
|
|
221
|
+
cloned.set(:isolated_key, "only in clone")
|
|
222
|
+
|
|
223
|
+
puts "cloned.key?(:isolated_key) = #{cloned.key?(:isolated_key)}"
|
|
224
|
+
puts "memory.key?(:isolated_key) = #{memory.key?(:isolated_key)} (isolated!)"
|
|
225
|
+
puts
|
|
226
|
+
|
|
227
|
+
# Show exactly what differs between clone and original
|
|
228
|
+
diff = Hashdiff.diff(memory.to_h, cloned.to_h)
|
|
229
|
+
puts "Diff (original vs clone):"
|
|
230
|
+
diff.each do |change|
|
|
231
|
+
op, path, *values = change
|
|
232
|
+
case op
|
|
233
|
+
when "+"
|
|
234
|
+
puts " + #{path}: #{values.first.inspect}"
|
|
235
|
+
when "-"
|
|
236
|
+
puts " - #{path}: #{values.first.inspect}"
|
|
237
|
+
when "~"
|
|
238
|
+
puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
puts
|
|
242
|
+
|
|
243
|
+
# =============================================================================
|
|
244
|
+
# Section 8: Delete and reserved key protection
|
|
245
|
+
# =============================================================================
|
|
246
|
+
|
|
247
|
+
puts "--- Section 8: Delete and Reserved Key Protection ---"
|
|
248
|
+
puts
|
|
249
|
+
|
|
250
|
+
before_delete = memory.to_h
|
|
251
|
+
memory.delete(:status)
|
|
252
|
+
|
|
253
|
+
puts "After memory.delete(:status):"
|
|
254
|
+
diff = Hashdiff.diff(before_delete, memory.to_h)
|
|
255
|
+
diff.each do |change|
|
|
256
|
+
op, path, *values = change
|
|
257
|
+
case op
|
|
258
|
+
when "-"
|
|
259
|
+
puts " - #{path}: #{values.first.inspect}"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
puts
|
|
263
|
+
|
|
264
|
+
begin
|
|
265
|
+
memory.delete(:data)
|
|
266
|
+
rescue ArgumentError => e
|
|
267
|
+
puts "memory.delete(:data) raises: #{e.message}"
|
|
268
|
+
end
|
|
269
|
+
puts
|
|
270
|
+
|
|
271
|
+
# =============================================================================
|
|
272
|
+
# Section 9: Clear vs Reset
|
|
273
|
+
# =============================================================================
|
|
274
|
+
|
|
275
|
+
puts "--- Section 9: Clear vs Reset ---"
|
|
276
|
+
puts
|
|
277
|
+
|
|
278
|
+
memory.set(:temp1, "value1")
|
|
279
|
+
memory.set(:temp2, "value2")
|
|
280
|
+
|
|
281
|
+
puts "Before clear:"
|
|
282
|
+
before_clear = memory.to_h
|
|
283
|
+
ap before_clear
|
|
284
|
+
puts
|
|
285
|
+
|
|
286
|
+
memory.clear
|
|
287
|
+
|
|
288
|
+
puts "After clear (non-reserved keys removed):"
|
|
289
|
+
after_clear = memory.to_h
|
|
290
|
+
ap after_clear
|
|
291
|
+
puts
|
|
292
|
+
|
|
293
|
+
puts "Diff (before clear vs after clear):"
|
|
294
|
+
Hashdiff.diff(before_clear, after_clear).each do |change|
|
|
295
|
+
op, path, *values = change
|
|
296
|
+
case op
|
|
297
|
+
when "+"
|
|
298
|
+
puts " + #{path}: #{values.first.inspect}"
|
|
299
|
+
when "-"
|
|
300
|
+
puts " - #{path}: #{values.first.inspect}"
|
|
301
|
+
when "~"
|
|
302
|
+
puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
puts
|
|
306
|
+
|
|
307
|
+
before_reset = memory.to_h
|
|
308
|
+
memory.reset
|
|
309
|
+
|
|
310
|
+
puts "After reset (full reset to initial state):"
|
|
311
|
+
after_reset = memory.to_h
|
|
312
|
+
ap after_reset
|
|
313
|
+
puts
|
|
314
|
+
|
|
315
|
+
puts "Diff (before reset vs after reset):"
|
|
316
|
+
Hashdiff.diff(before_reset, after_reset).each do |change|
|
|
317
|
+
op, path, *values = change
|
|
318
|
+
case op
|
|
319
|
+
when "+"
|
|
320
|
+
puts " + #{path}: #{values.first.inspect}"
|
|
321
|
+
when "-"
|
|
322
|
+
puts " - #{path}: #{values.first.inspect}"
|
|
323
|
+
when "~"
|
|
324
|
+
puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
puts
|
|
328
|
+
|
|
329
|
+
puts "=" * 70
|
|
330
|
+
puts "All sections completed without any LLM calls."
|
|
331
|
+
puts "=" * 70
|