Module: Insika::Refinement::ProposerFactory

Defined in:
lib/insika/refinement/proposer.rb

Overview

Resolves WHICH model(s) write the candidate, and builds the ask.

refinement.proposers  on the agent  (phase D: a PANEL, RFC-0013 §3.9)
-> refinement.proposer  ("deepseek/deepseek-chat" | "deepseek-chat")
-> the platform utility_model
-> nothing, and the caller refuses. There is no default model here on purpose:
 guessing one spends an operator's provider budget without being asked.

Class Method Summary collapse

Class Method Details

.build(config, utility_model: nil, ask_factory: nil, llm: nil) ⇒ Object

config: the agent's refinement hash. -> Proposer | nil (the FIRST of the panel — phase C's single-proposer entry point, kept because a deployment that never configured a panel is a panel of one).



194
195
196
# File 'lib/insika/refinement/proposer.rb', line 194

def build(config, utility_model: nil, ask_factory: nil, llm: nil)
  panel(config, utility_model: utility_model, ask_factory: ask_factory, llm: llm).first
end

.normalize_ref(entry) ⇒ Object



227
228
229
230
231
232
233
234
235
# File 'lib/insika/refinement/proposer.rb', line 227

def normalize_ref(entry)
  return Coercion.presence(entry) unless entry.is_a?(Hash)

  e = Coercion.deep_stringify(entry)
  model = Coercion.presence(e["model"])
  return nil if model.nil?

  (provider = Coercion.presence(e["provider"])) ? "#{provider}/#{model}" : model
end

.panel(config, utility_model: nil, ask_factory: nil, max: nil, llm: nil) ⇒ Object

-> [Proposer], in configured order, DEDUPED by model ref and capped at the RFC-0010 fan-out (§3.9 says the panel reuses it). Two entries naming the same model are one proposer: asking the same model twice at temperature 0 measures its variance, which is exactly what D6 rejected for the judges. llm (RFC-0017 A2): the graph's own RubyLLM context; nil = the global constant. Today only the deployment root builds a panel, and a deployment is one graph per process — the seam exists so an embedded graph that ever gains the refinement commands proposes on its own credentials.



206
207
208
209
210
211
212
213
214
# File 'lib/insika/refinement/proposer.rb', line 206

def panel(config, utility_model: nil, ask_factory: nil, max: nil, llm: nil)
  refs = refs_for(Coercion.deep_stringify(config || {}), utility_model)
  cap = max || Insika::SubagentGraph.fan_out_cap
  factory = ask_factory || ->(model, provider) { ruby_llm_ask(model, provider, llm: llm) }
  refs.first(cap).map do |ref|
    provider, model = split_ref(ref)
    Proposer.new(ask: factory.call(model, provider), model: ref)
  end
end

.refs_for(config, utility_model) ⇒ Object

proposers accepts either syntax — a bare ref ("deepseek/deepseek-chat") or the RFC's { "model" =>, "provider"? => } — because the two already coexist in this config (proposer is a bare ref, judges are hashes) and refusing one of them would only teach operators which page they were reading.



220
221
222
223
224
225
# File 'lib/insika/refinement/proposer.rb', line 220

def refs_for(config, utility_model)
  listed = Array(config["proposers"]).filter_map { |entry| normalize_ref(entry) }
  return listed.uniq unless listed.empty?

  [Coercion.presence(config["proposer"]) || Coercion.presence(utility_model)].compact
end

.ruby_llm_ask(model, provider, llm: nil) ⇒ Object

Temperature 0: a proposal an operator rejects should be re-proposable, and a gate result should be attributable to the edit rather than to a sampling seed. ruby_llm is required lazily so nothing loads a provider gem until a proposer is actually configured.

Returns the MESSAGE, not .content: the token counts ride on it and the budget (§3.9) is what spends them. Proposer reads either shape.



252
253
254
255
256
257
258
259
# File 'lib/insika/refinement/proposer.rb', line 252

def ruby_llm_ask(model, provider, llm: nil)
  require "ruby_llm"
  llm ||= RubyLLM
  lambda do |prompt|
    llm.chat(model: model, provider: provider, assume_model_exists: true)
       .with_temperature(0).ask(prompt)
  end
end

.split_ref(ref) ⇒ Object

"provider/model" -> [provider, model]; "model" -> [nil, model]. Same reading Safety::Factory uses for the moderator — one syntax for "which model", not one per feature.



240
241
242
243
# File 'lib/insika/refinement/proposer.rb', line 240

def split_ref(ref)
  prov, name = ref.to_s.split("/", 2)
  name ? [prov, name] : [nil, prov]
end