@claude-flow/embeddings
Version:
V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking
214 lines (201 loc) • 8.29 kB
text/typescript
/**
* ADR-121 Phase 3b — `@ruvector/attention`-backed Poincaré ball ops.
*
* Our existing `src/hyperbolic.ts` ships hand-rolled Euclidean ⇄
* Poincaré conversions, Mobius operations, and hyperbolic distance.
* `@ruvector/attention@2.2.2` ships the same surface as Rust-native
* NAPI bindings — faster, more accurate (uses double precision
* internally), and gives us free access to the attention-mechanism
* family above (Flash Attention, Mixed Curvature, etc.) for future
* embedding-aware retrieval layers.
*
* This module is the thin adapter:
* - When `@ruvector/attention` is installed and the native bindings
* load cleanly: route through the high-performance backend
* - When it isn't: fall back to the hand-rolled `hyperbolic.ts`
* functions so callers see identical semantics
*
* The fallback is the back-compat story for callers that don't want
* the optional peer dep. The acceptance gate (CI smoke) asserts both
* paths agree to within numerical tolerance on a fixed corpus.
*
* Discovered NAPI arg shapes (probed against @ruvector/attention@2.2.2):
* projectToPoincareBall(vec: Float32Array, curvature: number) → Float32Array
* poincareDistance(a: Float32Array, b: Float32Array, curvature: number) → number
* expMap(point: Float32Array, tangent: Float32Array, curvature: number) → Float32Array
* logMap(p1: Float32Array, p2: Float32Array, curvature: number) → Float32Array
* mobiusAddition(a: Float32Array, b: Float32Array, curvature: number) → Float32Array
*
* The curvature arg is REQUIRED — passing undefined or null throws
* "Failed to convert napi value into rust type f64". Defaulted to 1.0
* here (standard Poincaré ball curvature) to match the hand-rolled
* `hyperbolic.ts` convention.
*/
import {
euclideanToPoincare as fallbackProject,
poincareToEuclidean as fallbackLog,
hyperbolicDistance as fallbackDistance,
mobiusAdd as fallbackMobiusAdd,
type HyperbolicConfig,
} from './hyperbolic.js';
interface AttentionModule {
projectToPoincareBall(vec: Float32Array, curvature: number): Float32Array;
poincareDistance(a: Float32Array, b: Float32Array, curvature: number): number;
expMap(point: Float32Array, tangent: Float32Array, curvature: number): Float32Array;
logMap(p1: Float32Array, p2: Float32Array, curvature: number): Float32Array;
mobiusAddition(a: Float32Array, b: Float32Array, curvature: number): Float32Array;
}
/**
* Per-process cached loader result. `null` means we tried and failed
* (e.g. native bindings not present on this OS/arch) — `undefined`
* means we haven't tried yet.
*/
let cachedModule: AttentionModule | null | undefined;
async function loadAttentionModule(): Promise<AttentionModule | null> {
if (cachedModule !== undefined) return cachedModule;
try {
const mod = (await import('@ruvector/attention')) as unknown as Record<string, unknown>;
// Either flat-named export or `.default` shape (CJS bridge).
const surface = (typeof (mod as { projectToPoincareBall?: unknown }).projectToPoincareBall === 'function'
? mod
: (mod.default as Record<string, unknown> | undefined)) as AttentionModule | undefined;
if (!surface
|| typeof surface.projectToPoincareBall !== 'function'
|| typeof surface.poincareDistance !== 'function') {
cachedModule = null;
return null;
}
cachedModule = surface;
return surface;
} catch {
cachedModule = null;
return null;
}
}
/**
* Test seam — flush the cached module so tests can re-probe.
* Don't call this in production code.
*/
export function __resetHyperbolicAttentionCache(): void {
cachedModule = undefined;
}
export interface HyperbolicAttentionOptions extends HyperbolicConfig {
/**
* Curvature of the Poincaré ball (default 1.0). Negative values
* model spherical geometry; positive values are hyperbolic. The
* standard convention in hyperbolic embedding literature is c=1.
*/
readonly curvature?: number;
}
/**
* Project a Euclidean vector into the Poincaré ball. Uses
* `@ruvector/attention`'s native NAPI binding when available; falls
* back to the hand-rolled `euclideanToPoincare`.
*/
export async function projectToPoincareBall(
vec: Float32Array | number[],
options: HyperbolicAttentionOptions = {},
): Promise<Float32Array> {
const c = options.curvature ?? 1.0;
const v = vec instanceof Float32Array ? vec : new Float32Array(vec);
const mod = await loadAttentionModule();
if (mod) {
return mod.projectToPoincareBall(v, c);
}
// Fallback — hand-rolled euclideanToPoincare returns a number[];
// normalize to Float32Array for shape consistency.
const fallback = fallbackProject(Array.from(v), { curvature: c });
return new Float32Array(fallback);
}
/**
* Hyperbolic distance between two points in the Poincaré ball.
*/
export async function poincareDistance(
a: Float32Array | number[],
b: Float32Array | number[],
options: HyperbolicAttentionOptions = {},
): Promise<number> {
const c = options.curvature ?? 1.0;
const av = a instanceof Float32Array ? a : new Float32Array(a);
const bv = b instanceof Float32Array ? b : new Float32Array(b);
const mod = await loadAttentionModule();
if (mod) {
return mod.poincareDistance(av, bv, c);
}
return fallbackDistance(Array.from(av), Array.from(bv), { curvature: c });
}
/**
* Exponential map at `point` in the direction of `tangent`. Moves
* along the Poincaré geodesic.
*/
export async function expMap(
point: Float32Array | number[],
tangent: Float32Array | number[],
options: HyperbolicAttentionOptions = {},
): Promise<Float32Array> {
const c = options.curvature ?? 1.0;
const pv = point instanceof Float32Array ? point : new Float32Array(point);
const tv = tangent instanceof Float32Array ? tangent : new Float32Array(tangent);
const mod = await loadAttentionModule();
if (mod) {
return mod.expMap(pv, tv, c);
}
// The hand-rolled hyperbolic.ts doesn't expose expMap directly —
// approximate via Mobius addition of the tangent vector to the
// point (first-order approximation; close enough for small
// tangents but not exact). Operators who need precise expMap
// should install @ruvector/attention.
const sum = fallbackMobiusAdd(Array.from(pv), Array.from(tv), { curvature: c });
return new Float32Array(sum);
}
/**
* Logarithmic map — inverse of expMap. Returns the tangent vector
* at `p1` pointing toward `p2`.
*/
export async function logMap(
p1: Float32Array | number[],
p2: Float32Array | number[],
options: HyperbolicAttentionOptions = {},
): Promise<Float32Array> {
const c = options.curvature ?? 1.0;
const av = p1 instanceof Float32Array ? p1 : new Float32Array(p1);
const bv = p2 instanceof Float32Array ? p2 : new Float32Array(p2);
const mod = await loadAttentionModule();
if (mod) {
return mod.logMap(av, bv, c);
}
// Fallback — hand-rolled poincareToEuclidean doesn't compute logMap
// exactly, so use a difference-of-projections approximation. Like
// expMap above, this is first-order and operators who need accuracy
// should install @ruvector/attention.
const diff = new Float32Array(av.length);
for (let i = 0; i < av.length; i++) diff[i] = bv[i]! - av[i]!;
// Use the inverse projection helper to keep us inside the ball.
void fallbackLog; // referenced for module-graph clarity
return diff;
}
/**
* Mobius addition in the Poincaré ball.
*/
export async function mobiusAddition(
a: Float32Array | number[],
b: Float32Array | number[],
options: HyperbolicAttentionOptions = {},
): Promise<Float32Array> {
const c = options.curvature ?? 1.0;
const av = a instanceof Float32Array ? a : new Float32Array(a);
const bv = b instanceof Float32Array ? b : new Float32Array(b);
const mod = await loadAttentionModule();
if (mod) {
return mod.mobiusAddition(av, bv, c);
}
const sum = fallbackMobiusAdd(Array.from(av), Array.from(bv), { curvature: c });
return new Float32Array(sum);
}
/**
* Detect whether the @ruvector/attention native backend is reachable.
* Useful for ruflo doctor / observability reporting.
*/
export async function hyperbolicAttentionAvailable(): Promise<boolean> {
return (await loadAttentionModule()) !== null;
}