Prd/components/backend/lib/helpdesk/ledger.rb
Lucy Doupalů be9f14ce34 Helpdesk - operator console + patched GrapheneOS Dialer for call handling
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.
2026-07-27 18:50:32 +02:00

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