A small helpdesk system: an office Pixel running a patched GrapheneOS Dialer answers technician calls, records both call legs as separate channels, and a Ruby backend transcribes them through Whisper and files an AI summary against the caller. Squashed to a single commit for sharing. No credentials are included; secrets live outside the repo in /etc/helpdesk/env on the server or a gitignored .claude/env.local locally. See .claude/env.local.example for the shape. Start at README.md, then docs/architecture.md.
104 lines
4.6 KiB
Ruby
104 lines
4.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Helpdesk::Ledger — the PURE, network-free deterministic reducer that maintains a caller's yes/no
|
|
# "resolutions ledger". It never flips a past decision in place: a reversal appends a new entry, retains
|
|
# the old one (superseded_by), and flags a contradiction for a human. Matching is by canonical entry id
|
|
# (supplied by the summariser) with an embedding cosine backstop; vectors are passed IN (never embedded
|
|
# here) so this stays offline-testable and safe to call under the store lock. grep anchor: resolutions-ledger.
|
|
module Helpdesk
|
|
module Ledger
|
|
module_function
|
|
|
|
MATCH_THRESHOLD = 0.86 # cosine >= this = "same question" (calibrate offline against Czech paraphrases)
|
|
|
|
# --- vector packing: base64 Float32, single line even under JSON.pretty_generate ---
|
|
def pack_vec(floats) = [Array(floats).pack("g*")].pack("m0")
|
|
def unpack_vec(str)
|
|
return nil if str.to_s.empty?
|
|
str.unpack1("m0").unpack("g*")
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
|
|
def cosine(a, b)
|
|
return 0.0 unless a.is_a?(Array) && b.is_a?(Array) && a.length == b.length && !a.empty?
|
|
dot = na = nb = 0.0
|
|
i = 0
|
|
while i < a.length
|
|
dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; i += 1
|
|
end
|
|
d = Math.sqrt(na) * Math.sqrt(nb)
|
|
d.zero? ? 0.0 : dot / d
|
|
end
|
|
|
|
VALID_DECISIONS = %w[yes no conditional].freeze
|
|
def normalize_decision(x) = VALID_DECISIONS.include?(x.to_s) ? x.to_s : "conditional"
|
|
|
|
# Apply one call's ledger_deltas to `ledger`. Pure: no store, no network. Idempotent per event_id.
|
|
# Returns { ledger:, changed:, applied_event_ids: }.
|
|
def apply(ledger:, deltas:, event_id:, started_at:, applied_event_ids:, entry_vec:, next_id:)
|
|
applied = Array(applied_event_ids)
|
|
return { ledger: ledger, changed: false, applied_event_ids: applied } if applied.include?(event_id)
|
|
|
|
work = ledger.map(&:dup) # copy-on-write; never mutate caller's entry hashes
|
|
by_id = work.each_with_object({}) { |e, h| h[e["id"]] = e }
|
|
changed = false
|
|
|
|
Array(deltas).each_with_index do |dlt, idx|
|
|
decision = normalize_decision(dlt["decision"])
|
|
entry = resolve_entry(dlt, work, by_id, entry_vec)
|
|
|
|
if entry.nil?
|
|
e = { "id" => next_id.call, "question" => dlt["question"].to_s, "decision" => decision,
|
|
"first_at" => started_at, "last_at" => started_at, "last_event_id" => event_id,
|
|
"quote" => dlt["quote"].to_s, "vec_ref" => { "event_id" => event_id, "idx" => idx } }
|
|
# embed-failed (nil vector) can never be cosine-matched -> flag for human review, never silently merge
|
|
e["contradiction"] = true if dlt["question_vec"].nil?
|
|
work << e; by_id[e["id"]] = e; changed = true
|
|
elsif entry["decision"] == decision
|
|
entry["last_at"] = started_at; entry["last_event_id"] = event_id; changed = true
|
|
else
|
|
# different decision = reversal. Never mutate in place. Append the new decision.
|
|
neu = { "id" => next_id.call, "question" => entry["question"], "decision" => decision,
|
|
"first_at" => started_at, "last_at" => started_at, "last_event_id" => event_id,
|
|
"quote" => dlt["quote"].to_s, "vec_ref" => { "event_id" => event_id, "idx" => idx },
|
|
"contradiction" => true }
|
|
entry["contradiction"] = true
|
|
# only a strictly-LATER event supersedes the current decision (H1: out-of-order safe).
|
|
entry["superseded_by"] = neu["id"] if later?(started_at, entry["last_at"])
|
|
work << neu; by_id[neu["id"]] = neu; changed = true
|
|
end
|
|
end
|
|
|
|
{ ledger: work, changed: changed, applied_event_ids: applied + [event_id] }
|
|
end
|
|
|
|
# canonical id -> existing (non-superseded) entry; else cosine backstop; else nil (=> new entry).
|
|
def resolve_entry(dlt, work, by_id, entry_vec)
|
|
id = dlt["entry_id"].to_s
|
|
unless id.empty?
|
|
e = by_id[id]
|
|
return e if e && !e["superseded_by"] # unknown/foreign/superseded id -> fall through to backstop
|
|
end
|
|
qv = dlt["question_vec"]
|
|
qv = Ledger.unpack_vec(qv) if qv.is_a?(String)
|
|
return nil unless qv.is_a?(Array) && !qv.empty? && entry_vec
|
|
|
|
best = nil; best_sim = 0.0
|
|
work.each do |e|
|
|
next if e["superseded_by"]
|
|
ev = entry_vec.call(e)
|
|
s = Ledger.cosine(qv, ev)
|
|
(best = e; best_sim = s) if s > best_sim
|
|
end
|
|
best_sim >= MATCH_THRESHOLD ? best : nil
|
|
end
|
|
|
|
# strictly-later comparison on ISO-8601 strings; nil sorts oldest so a missing ts never supersedes.
|
|
def later?(a, b)
|
|
return false if a.nil?
|
|
return true if b.nil?
|
|
a.to_s > b.to_s
|
|
end
|
|
end
|
|
end
|