robot_lab-cyborg 0.2.7 → 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 +6 -0
- data/CHANGELOG.md +4 -0
- data/README.md +5 -5
- data/Rakefile +0 -1
- data/_typos.toml +7 -0
- data/examples/02_terminal_mentions.rb +8 -7
- data/examples/03_robot_interviews_cyborg.rb +9 -7
- data/lib/robot_lab/cyborg/channel.rb +2 -0
- data/lib/robot_lab/cyborg/conversation.rb +2 -0
- data/lib/robot_lab/cyborg/interviewer.rb +2 -0
- data/lib/robot_lab/cyborg/version.rb +1 -1
- data/lib/robot_lab/cyborg.rb +22 -0
- metadata +5 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dfbcf7adcfb8b32603238e8a5eb110c99398669178dda48509e8311effac3f57
|
|
4
|
+
data.tar.gz: f61c2ed79996d438c3e19169a39f93fb182d6c375ee817687c0d8279fc057c32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0825678ce77712d0ed11e87b892fdb820a4a71d3d4b32829135de88d017facde8a0387d600d30e21576c2696c0cae4708bf7a58ec3430c5b926b295147edcdf4'
|
|
7
|
+
data.tar.gz: d4448896b249569e150f7af9fc33c57820b34ac36a77db363547e6c97bf18d0904d3e4d4d79e72dda7e08fb2bbc176bff30e67aff7fc381171a0641280fc6739
|
data/.envrc
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.8] - 2026-09-09
|
|
11
|
+
|
|
12
|
+
Released in lockstep with `robot_lab` core v0.2.8: this gem now resolves the released core gem from RubyGems instead of the local sibling checkout (local-path development remains available via `BUNDLE_GEMFILE=Gemfile.local`). Also in this release: 17 reek false positives annotated inline, reek added to the development bundle, gem lifecycle tasks moved to asgard, and the release `Gemfile` no longer overrides `robot_lab` with the local checkout.
|
|
13
|
+
|
|
10
14
|
### Fixed (from the design/architecture review)
|
|
11
15
|
- **Consumer no longer wedges on a handler error.** An exception in an
|
|
12
16
|
`on_initiative`/channel path used to kill the Interviewer's consumer thread and
|
data/README.md
CHANGED
|
@@ -112,8 +112,8 @@ A human step currently holds a thread while it waits. `ask_async` returns the pe
|
|
|
112
112
|
Runnable demos in [`examples/`](examples) — one feature area each:
|
|
113
113
|
|
|
114
114
|
- `01_human_in_the_network.rb` — a human as a pipeline step, peers messaging over a bus, shared memory (network → human). Key-free.
|
|
115
|
-
- `02_terminal_mentions.rb` — a **live** human addresses peers by `@mention` via the library `Conversation` (fan-out, and no-mention broadcast); replies return on their own through the duplex. Includes a real **LLM robot** (`@assistant`,
|
|
116
|
-
- `03_robot_interviews_cyborg.rb` — the Interviewer the *other* way round: an **LLM robot** (
|
|
115
|
+
- `02_terminal_mentions.rb` — a **live** human addresses peers by `@mention` via the library `Conversation` (fan-out, and no-mention broadcast); replies return on their own through the duplex. Includes a real **LLM robot** (`@assistant`, LM Studio via the :lms provider) cooperating via `serve`, plus key-free canned peers.
|
|
116
|
+
- `03_robot_interviews_cyborg.rb` — the Interviewer the *other* way round: an **LLM robot** (LM Studio via the :lms provider) interviews the human via `delegate`, starting with a **typed** intake (`ask_confirm`/`ask_int`, which re-ask on bad input), then builds a categorized profile.
|
|
117
117
|
- `04_presence_and_availability.rb` — routing to a peer who's actually there: `online`/`away`/`offline`, an offline human declining immediately, and a bounded-timeout escalation. Key-free.
|
|
118
118
|
- `05_listening_and_duplex.rb` — always-on `listen`: the human speaks to the network **unprompted** and replies come back on the channel — both directions handled by the library. Key-free.
|
|
119
119
|
|
|
@@ -122,9 +122,9 @@ ruby examples/01_human_in_the_network.rb
|
|
|
122
122
|
ruby examples/04_presence_and_availability.rb
|
|
123
123
|
ruby examples/05_listening_and_duplex.rb
|
|
124
124
|
|
|
125
|
-
# Examples 2 and 3 use a real robot on
|
|
126
|
-
#
|
|
127
|
-
# without it — only @assistant needs
|
|
125
|
+
# Examples 2 and 3 use a real robot on LM Studio (lms server start &&
|
|
126
|
+
# lms get openai/gpt-oss-20b; override with LMS_MODEL / LMS_API_BASE). In
|
|
127
|
+
# example 2 the canned peers still work without it — only @assistant needs it.
|
|
128
128
|
ruby examples/02_terminal_mentions.rb # then type: @analyst and @scribe: status?
|
|
129
129
|
ruby examples/03_robot_interviews_cyborg.rb # the robot asks you the questions
|
|
130
130
|
```
|
data/Rakefile
CHANGED
data/_typos.toml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# False-positive suppressions for the typos quality gate (asgard).
|
|
2
|
+
# Everything here was reviewed by hand — see git history for context.
|
|
3
|
+
|
|
4
|
+
[default.extend-identifiers]
|
|
5
|
+
# Docs and comments hyphenate "mis-attributed"/"mis-attribution"; typos
|
|
6
|
+
# tokenizes the prefix alone and flags it.
|
|
7
|
+
mis = "mis"
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
# delivers inbound bus traffic to its channel (the output half of the duplex),
|
|
14
14
|
# so this demo never has to poll or render replies.
|
|
15
15
|
#
|
|
16
|
-
# @assistant is a real LLM robot (
|
|
17
|
-
# one-call symmetric responder. @analyst and @scribe are key-free
|
|
18
|
-
# Set
|
|
19
|
-
# peers (@assistant will simply not answer).
|
|
16
|
+
# @assistant is a real LLM robot (LM Studio via :lms) that cooperates via
|
|
17
|
+
# #serve — the one-call symmetric responder. @analyst and @scribe are key-free
|
|
18
|
+
# canned peers. Set LMS_API_BASE/LMS_MODEL, or run without LM Studio and just
|
|
19
|
+
# use the canned peers (@assistant will simply not answer).
|
|
20
20
|
#
|
|
21
21
|
# ruby examples/02_terminal_mentions.rb
|
|
22
22
|
# # then type: @analyst and @scribe: status?
|
|
@@ -31,13 +31,14 @@ core_lib = File.expand_path("../../robot_lab/lib", __dir__)
|
|
|
31
31
|
$LOAD_PATH.unshift(core_lib) if File.directory?(core_lib)
|
|
32
32
|
|
|
33
33
|
require "robot_lab"
|
|
34
|
+
require "ruby_llm/providers/lms"
|
|
34
35
|
require_relative "../lib/robot_lab/cyborg"
|
|
35
36
|
|
|
36
37
|
Cyborg = RobotLab::Cyborg
|
|
37
38
|
Channel = RobotLab::Cyborg::Channel
|
|
38
39
|
|
|
39
40
|
RubyLLM.configure do |c|
|
|
40
|
-
c.
|
|
41
|
+
c.lms_api_base = ENV.fetch("LMS_API_BASE", "http://localhost:1234/v1")
|
|
41
42
|
c.logger = Logger.new(File::NULL)
|
|
42
43
|
end
|
|
43
44
|
RobotLab.configure { |c| c.logger = Logger.new(File::NULL) }
|
|
@@ -69,8 +70,8 @@ you = Cyborg.new(name: "you", bus: bus, channel: Channel::Terminal.new(name: "ne
|
|
|
69
70
|
|
|
70
71
|
# A real LLM robot that serves bus tasks — the symmetric counterpart to how the
|
|
71
72
|
# Cyborg answers its human. One call, no hand-wired on_message.
|
|
72
|
-
RobotLab.build(name: "assistant", bus: bus, provider: "
|
|
73
|
-
model: ENV.fetch("
|
|
73
|
+
RobotLab.build(name: "assistant", bus: bus, provider: "lms",
|
|
74
|
+
model: ENV.fetch("LMS_MODEL", "openai/gpt-oss-20b"),
|
|
74
75
|
system_prompt: "You are a concise teammate. Answer in 1-2 sentences.").serve
|
|
75
76
|
|
|
76
77
|
# Two key-free canned peers.
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
# You can answer each question, type "skip" to pass on one, or "done" to end the
|
|
15
15
|
# interview early. The robot builds the profile from whatever you chose to share.
|
|
16
16
|
#
|
|
17
|
-
# Requires a running
|
|
18
|
-
#
|
|
17
|
+
# Requires a running LM Studio server with the model downloaded:
|
|
18
|
+
# lms server start && lms get openai/gpt-oss-20b
|
|
19
|
+
# Override with LMS_MODEL / LMS_API_BASE if yours differ.
|
|
19
20
|
#
|
|
20
21
|
# ruby examples/03_robot_interviews_cyborg.rb
|
|
21
22
|
|
|
@@ -25,20 +26,21 @@ core_lib = File.expand_path("../../robot_lab/lib", __dir__)
|
|
|
25
26
|
$LOAD_PATH.unshift(core_lib) if File.directory?(core_lib)
|
|
26
27
|
|
|
27
28
|
require "robot_lab"
|
|
29
|
+
require "ruby_llm/providers/lms"
|
|
28
30
|
require_relative "../lib/robot_lab/cyborg"
|
|
29
31
|
|
|
30
32
|
Cyborg = RobotLab::Cyborg
|
|
31
33
|
Channel = RobotLab::Cyborg::Channel
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
LMS_API_BASE = ENV.fetch("LMS_API_BASE", "http://localhost:1234/v1")
|
|
36
|
+
LMS_MODEL = ENV.fetch("LMS_MODEL", "openai/gpt-oss-20b")
|
|
35
37
|
MAX_QUESTIONS = Integer(ENV.fetch("MAX_QUESTIONS", "5"))
|
|
36
38
|
|
|
37
39
|
ENDING_WORDS = %w[done stop quit exit].freeze
|
|
38
40
|
SKIPPING_WORDS = %w[skip pass].freeze
|
|
39
41
|
|
|
40
42
|
RubyLLM.configure do |c|
|
|
41
|
-
c.
|
|
43
|
+
c.lms_api_base = LMS_API_BASE
|
|
42
44
|
c.logger = Logger.new(File::NULL)
|
|
43
45
|
end
|
|
44
46
|
RobotLab.configure { |c| c.logger = Logger.new(File::NULL) }
|
|
@@ -48,8 +50,8 @@ RobotLab.configure { |c| c.logger = Logger.new(File::NULL) }
|
|
|
48
50
|
# single Interviewer ask.
|
|
49
51
|
robot = RobotLab.build(
|
|
50
52
|
name: "ProfileBot",
|
|
51
|
-
provider: "
|
|
52
|
-
model:
|
|
53
|
+
provider: "lms",
|
|
54
|
+
model: LMS_MODEL,
|
|
53
55
|
system_prompt: <<~PROMPT
|
|
54
56
|
You are ProfileBot, a warm, concise interviewer. Your goal is to learn as much
|
|
55
57
|
as the person is willing to share so you can later classify them across these
|
|
@@ -19,6 +19,8 @@ module RobotLab
|
|
|
19
19
|
# @!attribute kind [Symbol] :question | :answer | :message | :notice
|
|
20
20
|
# @!attribute at [Time] when the message was created
|
|
21
21
|
ChannelMessage = Data.define(:id, :content, :in_reply_to, :sender, :kind, :at) do
|
|
22
|
+
# :reek:ControlParameter -- nil at means "stamp with Time.now"; a default,
|
|
23
|
+
# not a behavior switch.
|
|
22
24
|
def initialize(content:, id: nil, in_reply_to: nil, sender: nil, kind: :message, at: nil)
|
|
23
25
|
super(id:, content: content.to_s, in_reply_to:, sender:, kind:, at: at || Time.now)
|
|
24
26
|
end
|
|
@@ -62,6 +62,8 @@ module RobotLab
|
|
|
62
62
|
#
|
|
63
63
|
# @param line [String]
|
|
64
64
|
# @return [Array<String>] the peers the message was sent to
|
|
65
|
+
# :reek:FeatureEnvy -- parsing the human's raw line (strip/scan for
|
|
66
|
+
# mentions) is this router's whole job; the String has no better home.
|
|
65
67
|
def route(line)
|
|
66
68
|
line = line.to_s.strip
|
|
67
69
|
return [] if line.empty?
|
|
@@ -155,6 +155,8 @@ module RobotLab
|
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
# If +id+ was the active question, clear it and deliver the next queued one.
|
|
158
|
+
# :reek:ControlParameter -- id is correlation data compared against
|
|
159
|
+
# @active_id to decide whether this question held the wire; not a mode flag.
|
|
158
160
|
def release_active(id)
|
|
159
161
|
advanced = @mutex.synchronize do
|
|
160
162
|
next false unless @active_id == id
|
data/lib/robot_lab/cyborg.rb
CHANGED
|
@@ -36,6 +36,11 @@ module RobotLab
|
|
|
36
36
|
# analyst.send_message(to: :dewayne, content: "Approve deploy? (yes/no)")
|
|
37
37
|
# # dewayne's human is prompted; the answer is sent back as a reply
|
|
38
38
|
#
|
|
39
|
+
# :reek:TooManyInstanceVariables :reek:TooManyMethods -- a Cyborg is a full
|
|
40
|
+
# network peer: it must carry the ivars the BusMessaging mixin expects plus
|
|
41
|
+
# its own channel/interviewer/presence state, and mirror Robot's member
|
|
42
|
+
# surface (run/call/delegate/remember/recall) so humans and robots are
|
|
43
|
+
# interchangeable.
|
|
39
44
|
class Cyborg
|
|
40
45
|
include RobotLab::Robot::BusMessaging
|
|
41
46
|
|
|
@@ -72,6 +77,10 @@ module RobotLab
|
|
|
72
77
|
# @param memory [RobotLab::Memory, nil] standalone memory (default: fresh)
|
|
73
78
|
# @param ask_timeout [Numeric, nil] seconds to wait for the human before
|
|
74
79
|
# giving up on an answer (nil = wait indefinitely)
|
|
80
|
+
# :reek:BooleanParameter -- auto_reply is a stored config flag, read later
|
|
81
|
+
# by the bus handlers, not a mode switch inside this method.
|
|
82
|
+
# :reek:ControlParameter -- nil memory/channel/interviewer mean "build the
|
|
83
|
+
# default"; each `||` is a fallback, not a behavior switch.
|
|
75
84
|
def initialize(name:, bus: nil, channel: nil, interviewer: nil,
|
|
76
85
|
auto_reply: true, memory: nil, ask_timeout: nil)
|
|
77
86
|
@name = name.to_s
|
|
@@ -136,6 +145,8 @@ module RobotLab
|
|
|
136
145
|
# @param network_memory [RobotLab::Memory, nil] shared memory when in a network
|
|
137
146
|
# @param memory [RobotLab::Memory, nil] explicit memory override
|
|
138
147
|
# @return [RobotResult]
|
|
148
|
+
# :reek:ControlParameter -- nil memory/network_memory fall back to this
|
|
149
|
+
# peer's own memory; defaults, not behavior switches.
|
|
139
150
|
def run(message = nil, network_memory: nil, memory: nil, **_kwargs)
|
|
140
151
|
active = memory || network_memory || @memory
|
|
141
152
|
attach_memory(network_memory) if network_memory
|
|
@@ -159,6 +170,10 @@ module RobotLab
|
|
|
159
170
|
# answer is acceptable, or nil to reject and re-ask
|
|
160
171
|
# @param retries [Integer] extra attempts allowed when validation rejects
|
|
161
172
|
# @return [Object, nil] the human's answer (coerced when validated)
|
|
173
|
+
# :reek:LongParameterList { max_params: 6 } -- one keyword per aspect of a
|
|
174
|
+
# question (choices/default/timeout/validation/retries).
|
|
175
|
+
# :reek:TooManyStatements -- the ask/validate/re-ask retry loop, one step
|
|
176
|
+
# per line; flog gates its real complexity.
|
|
162
177
|
def ask(question, choices: nil, default: nil, timeout: @ask_timeout, validate: nil, retries: 2)
|
|
163
178
|
attempt = 0
|
|
164
179
|
loop do
|
|
@@ -315,6 +330,9 @@ module RobotLab
|
|
|
315
330
|
# @param task [String] the task message
|
|
316
331
|
# @param async [Boolean] when true, returns a DelegationFuture immediately
|
|
317
332
|
# @return [RobotResult, DelegationFuture]
|
|
333
|
+
# :reek:BooleanParameter :reek:ControlParameter :reek:TooManyStatements --
|
|
334
|
+
# mirrors Robot#delegate's API and shape exactly: async picks the
|
|
335
|
+
# future-vs-sync return, and the two branches are the whole method.
|
|
318
336
|
def delegate(to:, task:, async: false, **)
|
|
319
337
|
if async
|
|
320
338
|
future = DelegationFuture.new(robot_name: to.name, delegated_by: @name)
|
|
@@ -353,6 +371,8 @@ module RobotLab
|
|
|
353
371
|
# @param key [Object]
|
|
354
372
|
# @param wait [Boolean, Numeric] false, true, or seconds to wait
|
|
355
373
|
# @return [Object, nil]
|
|
374
|
+
# :reek:BooleanParameter -- wait is forwarded verbatim to Memory#get, whose
|
|
375
|
+
# API it belongs to.
|
|
356
376
|
def recall(key, wait: false)
|
|
357
377
|
current_memory.get(key, wait: wait)
|
|
358
378
|
end
|
|
@@ -407,6 +427,8 @@ module RobotLab
|
|
|
407
427
|
|
|
408
428
|
# Run the block with +memory+'s current writer set to this peer, restoring it
|
|
409
429
|
# afterward. A no-op for memories that don't track a writer.
|
|
430
|
+
# :reek:FeatureEnvy -- save/set/restore of the writer on whichever memory is
|
|
431
|
+
# active for this run; the bookkeeping is the wrapper's job, not Memory's.
|
|
410
432
|
def with_writer(memory)
|
|
411
433
|
return yield unless memory.respond_to?(:current_writer=)
|
|
412
434
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robot_lab-cyborg
|
|
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
|
|
@@ -15,20 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
19
|
-
- - ">="
|
|
20
|
-
- !ruby/object:Gem::Version
|
|
21
|
-
version: 0.2.6
|
|
18
|
+
version: 0.3.0
|
|
22
19
|
type: :runtime
|
|
23
20
|
prerelease: false
|
|
24
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
22
|
requirements:
|
|
26
23
|
- - "~>"
|
|
27
24
|
- !ruby/object:Gem::Version
|
|
28
|
-
version:
|
|
29
|
-
- - ">="
|
|
30
|
-
- !ruby/object:Gem::Version
|
|
31
|
-
version: 0.2.6
|
|
25
|
+
version: 0.3.0
|
|
32
26
|
description: 'Adds a human peer worker to RobotLab networks: same bus and memory,
|
|
33
27
|
receiving and issuing tasks alongside robots.'
|
|
34
28
|
email:
|
|
@@ -45,6 +39,7 @@ files:
|
|
|
45
39
|
- LICENSE.txt
|
|
46
40
|
- README.md
|
|
47
41
|
- Rakefile
|
|
42
|
+
- _typos.toml
|
|
48
43
|
- docs/api_reference.md
|
|
49
44
|
- docs/custom_channels.md
|
|
50
45
|
- docs/getting_started.md
|
|
@@ -84,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
84
79
|
- !ruby/object:Gem::Version
|
|
85
80
|
version: '0'
|
|
86
81
|
requirements: []
|
|
87
|
-
rubygems_version: 4.0.
|
|
82
|
+
rubygems_version: 4.0.21
|
|
88
83
|
specification_version: 4
|
|
89
84
|
summary: Cyborg extension for the RobotLab multi-robot LLM orchestration framework.
|
|
90
85
|
test_files: []
|