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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5289e0bf089059f451300a62b776b06141d684bb60ed6df407624313a695500
4
- data.tar.gz: eab4d957cbd19ca2fb66e97d2ba2ddd806167c0dab6ffb623fad10204386cdd4
3
+ metadata.gz: dfbcf7adcfb8b32603238e8a5eb110c99398669178dda48509e8311effac3f57
4
+ data.tar.gz: f61c2ed79996d438c3e19169a39f93fb182d6c375ee817687c0d8279fc057c32
5
5
  SHA512:
6
- metadata.gz: f6a29be1c412e3005a55e25bfaacfbb60d6a535a636e9692f188da47ee09da8736268561570beba5d2164066dca8abbbac68f8f538a8171be2815697e8929d41
7
- data.tar.gz: 01b49e8eed2356da4eafb1bf7e48dc408a84e877bcd52aae5e5a75a80927dab83a8d4daf73ad875199cf44ef5e5bcf33b8d9b0aa2379d7e8d9b52433ba7bf17c
6
+ metadata.gz: '0825678ce77712d0ed11e87b892fdb820a4a71d3d4b32829135de88d017facde8a0387d600d30e21576c2696c0cae4708bf7a58ec3430c5b926b295147edcdf4'
7
+ data.tar.gz: d4448896b249569e150f7af9fc33c57820b34ac36a77db363547e6c97bf18d0904d3e4d4d79e72dda7e08fb2bbc176bff30e67aff7fc381171a0641280fc6739
data/.envrc CHANGED
@@ -1 +1,7 @@
1
+ # robot_lab_project/robot_lab-cyborg/.envrc
2
+
3
+ source_up
4
+
1
5
  export RR=$(pwd)
6
+
7
+ # BUNDLE_GEMFILE is inherited from the project root .envrc (asgard dev|prod)
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`, Ollama) cooperating via `serve`, plus key-free canned peers.
116
- - `03_robot_interviews_cyborg.rb` — the Interviewer the *other* way round: an **LLM robot** (Ollama) 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.
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 Ollama (ollama pull qwen3.6; override with
126
- # OLLAMA_MODEL / OLLAMA_API_BASE). In example 2 the canned peers still work
127
- # without it — only @assistant needs Ollama.
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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
3
  require 'rake/testtask'
5
4
 
6
5
  Rake::TestTask.new(:test) do |t|
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 (Ollama) that cooperates via #serve — the
17
- # one-call symmetric responder. @analyst and @scribe are key-free canned peers.
18
- # Set OLLAMA_API_BASE/OLLAMA_MODEL, or run without Ollama and just use the canned
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.ollama_api_base = ENV.fetch("OLLAMA_API_BASE", "http://localhost:11434/v1")
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: "ollama",
73
- model: ENV.fetch("OLLAMA_MODEL", "qwen3.6"),
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 Ollama with the model pulled: ollama pull qwen3.6
18
- # Override with OLLAMA_MODEL / OLLAMA_API_BASE if yours differ.
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
- OLLAMA_API_BASE = ENV.fetch("OLLAMA_API_BASE", "http://localhost:11434/v1")
34
- OLLAMA_MODEL = ENV.fetch("OLLAMA_MODEL", "qwen3.6")
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.ollama_api_base = OLLAMA_API_BASE
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: "ollama",
52
- model: OLLAMA_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
@@ -4,6 +4,6 @@ module RobotLab
4
4
  # RobotLab::Cyborg is a class (a human peer worker), so its VERSION and the
5
5
  # nested helper classes hang off the class itself rather than a module.
6
6
  class Cyborg
7
- VERSION = "0.2.7"
7
+ VERSION = "0.3.0"
8
8
  end
9
9
  end
@@ -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.2.7
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: '0.2'
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: '0.2'
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.19
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: []