lakehouse/tests/multi-agent/normalize.test.ts
root 52561d10d3 Input normalizer + unified memory query — "seamless with whatever input"
J asked directly: "did we implement our memory findings so that our
knowledge base and our configuration playbook [work] seamlessly with
whatever input they're given?" Honest answer tonight was "one of five
findings shipped, normalizer is the blocker." This closes that gap.

NORMALIZER (tests/multi-agent/normalize.ts):
Accepts structured JSON, natural language, or mixed. Returns canonical
NormalizedInput { role, city, state, count, client, deadline, intent,
confidence, extraction_method, missing_fields } for any downstream
consumer.

Three-tier path:
  1. Structured fast-path — already-shaped input skips LLM
  2. Regex path — "need 3 welders in Nashville, TN" parses without LLM.
     City/state parser tightened to 1-3 capitalized words + "in {city}"
     anchor preference + case-exact full-state-name variants to prevent
     "Forklift Operators in Chicago" being captured as the city name
  3. LLM fallback — qwen3 local with think:false + 400 max_tokens for
     inputs the regex can't handle

Unit tests (tests/multi-agent/normalize.test.ts): 9/9 pass. Covers
structured fast-path, misplacement→rescue intent, state-name→abbrev
conversion, regex extraction from natural language, plural role +
full state name edge case, rescue intent keyword precedence, partial
input reporting missing fields, empty object fallthrough, async/sync
parity on clean inputs.

UNIFIED MEMORY QUERY (tests/multi-agent/memory_query.ts):
One function, five parallel fan-outs, one bundle returned:
  - playbook_workers — hybrid_search via gateway with use_playbook_memory
  - pathway_recommendation — KB recommender for this sig
  - neighbor_signatures — K-NN sigs weighted by staffer competence
  - prior_lessons — T3 overseer lessons filtered by city/state
  - top_staffers — competence-sorted leaderboard
  - discovered_patterns — top workers endorsed across past playbooks
    for this (role, city, state)
  - latency_ms — per-source + total
Every branch is best-effort: one source down doesn't break the bundle.

HTTP ENDPOINT (mcp-server/index.ts):
  POST /memory/query with body {input: <anything>} → MemoryQueryResult
Returns the same shape the TS function does. Typed with types.ts for
future UI consumption.

VERIFIED:
  curl POST /memory/query with structured {role,city,state,count}
    → extraction_method=structured, 10 playbook workers, top score 0.878
  curl POST /memory/query with "I need 3 welders in Nashville, TN"
    → extraction_method=regex (no LLM call), 319ms total, 8 endorsements
      for Lauren Gomez auto-discovered as top Nashville Welder

Honest remaining gaps (documented for next phase):
  - Mem0 ADD/UPDATE/DELETE/NOOP — we still only ADD + mark_failed
  - Zep validity windows — playbook entries have timestamps but no
    retirement semantic
  - Letta working-memory / hot cache — every query scans all 1560
    playbook entries
  - Memory profiles / scoped queries — global pool, no per-staffer
    private subsets

2 of 5 findings now shipped (multi-strategy retrieval in Rust, input
normalization + unified query in TS). The remaining 3 are architectural
additions queued as Phase 25 items — validity windows first since it's
the most load-bearing for long-running systems.
2026-04-20 23:59:05 -05:00

78 lines
2.8 KiB
TypeScript

import { test, expect } from "bun:test";
import { normalizeInputSync, normalizeInput } from "./normalize.ts";
test("structured FillEvent → fast path", () => {
const n = normalizeInputSync({
kind: "baseline_fill", role: "Welder", city: "Nashville",
state: "TN", count: 4,
});
expect(n.extraction_method).toBe("structured");
expect(n.role).toBe("Welder");
expect(n.city).toBe("Nashville");
expect(n.state).toBe("TN");
expect(n.count).toBe(4);
expect(n.confidence).toBe("high");
expect(n.missing_fields.length).toBe(0);
});
test("misplacement kind → rescue intent", () => {
const n = normalizeInputSync({ kind: "misplacement", role: "Welder", city: "Nashville", state: "TN", count: 1 });
expect(n.intent).toBe("rescue");
});
test("structured with full state name normalizes to abbrev", () => {
const n = normalizeInputSync({ role: "Welder", city: "Nashville", state: "Tennessee" });
expect(n.state).toBe("TN");
});
test("natural-language regex path extracts count + role + city + state", () => {
const n = normalizeInputSync("I need 3 Welders in Nashville, TN for next week");
expect(n.role).toBe("Welder");
expect(n.city).toBe("Nashville");
expect(n.state).toBe("TN");
expect(n.count).toBe(3);
expect(n.intent).toBe("fill");
expect(n.extraction_method).toBe("regex");
});
test("natural language with plural role + full state name", () => {
const n = normalizeInputSync("fill 5 Forklift Operators in Chicago, Illinois");
expect(n.role).toBe("Forklift Operator");
expect(n.city).toBe("Chicago");
expect(n.state).toBe("IL");
expect(n.count).toBe(5);
});
test("rescue intent keyword beats fill", () => {
const n = normalizeInputSync("we need to rescue the Nashville welder fill, 2 workers");
expect(n.intent).toBe("rescue");
expect(n.count).toBe(2);
expect(n.role).toBe("Welder");
});
test("partial input reports missing fields with confidence=low or medium", () => {
const n = normalizeInputSync("need welders");
expect(n.role).toBe("Welder");
expect(n.city).toBeNull();
expect(n.state).toBeNull();
expect(n.count).toBeNull();
expect(n.missing_fields).toContain("city");
expect(n.missing_fields).toContain("state");
expect(n.confidence).toBe("low");
});
test("empty object does NOT go through structured path (no role or city)", () => {
const n = normalizeInputSync({});
expect(n.extraction_method).toBe("regex");
});
test("async normalizeInput on clean structured input matches sync", async () => {
const input = { role: "Welder", city: "Nashville", state: "TN", count: 3 };
const sync = normalizeInputSync(input);
const async_ = await normalizeInput(input);
expect(async_.role).toBe(sync.role);
expect(async_.city).toBe(sync.city);
expect(async_.state).toBe(sync.state);
expect(async_.count).toBe(sync.count);
});