Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
- The format is based on [Keep a Changelog](https://keepachangelog.com/).
- This project adheres to [Semantic Versioning](https://semver.org/).

## Version 1.2.0 - tbd

### Added

- Support `VECTOR_EMBEDDING` function using ONNX Runtime with `Xenova/all-MiniLM-L6-v2` model (384 dimensions)
- Downloads model on-demand from Hugging Face (~10MB, cached locally)
- Supports both 3-parameter `(text, text_type, model_and_version)` and 4-parameter variants with `remote_source`
- Compatible with `SAP_GXY.20250407` and `SAP_GXY.20240715` model versions
- Synchronous execution suitable for SQLite user-defined functions
- **Note**: Produces 384-dimensional vectors (vs. 768 in SAP HANA) for efficiency in local development scenarios



## Version 1.1.0 - 2026-07-20

Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,48 @@ resources:
type: org.cloudfoundry.managed-service
```

### 3. Vector Embedding API for Plugin Integration

The `@cap-js/ai` plugin exports a standalone vector embedding function that can be used by other plugins (like `@cap-js/sqlite`) to generate embeddings using an ONNX model.

#### Usage

```javascript
import { vector_embedding } from '@cap-js/ai/vector-embedding';

// Model initializes automatically on import - just use it
const embeddingJSON = vector_embedding('Hello world', 'DOCUMENT', 'SAP_GXY.20250407');
const embedding = JSON.parse(embeddingJSON); // Array of 384 float values
```

#### Function Signature

```typescript
function vector_embedding(
text: string | null,
text_type: string,
model_and_version: string
): string
```

**Parameters:**
- `text` - Text to embed (returns zero vector if null or empty)
- `text_type` - Type of text, e.g., `'DOCUMENT'` (currently informational)
- `model_and_version` - Model identifier, e.g., `'SAP_GXY.20250407'` or `'SAP_GXY.20240715'`

**Returns:**
- JSON stringified array of embedding values (384 dimensions)

**Features:**
- **Auto-initialization**: ONNX model loads automatically when module is imported (top-level await)
- **Deterministic**: Same input always produces same output
- **Normalized vectors**: All embeddings are L2-normalized
- **Semantic similarity**: Embeddings capture text meaning for similarity search

**Error Handling:**
- Throws if ONNX model failed to load during import
- Throws if embedding generation fails
- Import errors can be caught to detect if AI plugin is available

## Test the plugin locally

Expand Down
249 changes: 249 additions & 0 deletions lib/vector_embedding/InferenceSession.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Copy from onnxruntime-common/dist/cjs/inference-session-impl.js and referenced files by it
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// Adjusted to meet the needs of SQLite by making the run functions synchronous to avoid WorkerThreads
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const ort = require('onnxruntime-common');
const binding = require('onnxruntime-node/dist/binding.js');

class InferenceSession {
constructor(handler) {
this.handler = handler;
}

run(feeds) {
const fetches = {};
let options = {};
// check inputs
if (
typeof feeds !== 'object' ||
feeds === null ||
feeds instanceof ort.Tensor ||
Array.isArray(feeds)
) {
throw new TypeError(
"'feeds' must be an object that use input names as keys and OnnxValue as corresponding values."
);
}
// check if all inputs are in feed
for (const name of this.handler.inputNames) {
if (typeof feeds[name] === 'undefined')
throw new Error(`input '${name}' is missing in 'feeds'.`);
}
// if no fetches is specified, we use the full output names list
for (const name of this.handler.outputNames) {
fetches[name] = null;
}
// feeds, fetches and options are prepared
const results = this.handler.run(feeds, fetches, options);
const returnValue = {};
for (const key in results) {
if (Object.hasOwnProperty.call(results, key)) {
const result = results[key];
if (result instanceof ort.Tensor) returnValue[key] = result;
else returnValue[key] = new ort.Tensor(result.type, result.data, result.dims);
}
}
return returnValue;
}

static async create(arg0) {
let filePathOrUint8Array;
if (arg0 instanceof Uint8Array) filePathOrUint8Array = arg0;
else
throw Error(
'Argument is not supported. Check original InferenceSession implementation if this adjustment needs to be adopted'
);

// resolve backend, update session options with validated EPs, and create session handler
const [backend, optionsWithValidatedEPs] = await resolveBackendAndExecutionProviders();
const handler = await backend.createInferenceSessionHandler(
filePathOrUint8Array,
optionsWithValidatedEPs
);
return new InferenceSession(handler);
}
}

// Copy from onnxruntime-common/dist/cjs/backend-impl.js
async function resolveBackendAndExecutionProviders() {
const backends = new Map();
const backendsList = listSupportedBackends();
for (const backend of backendsList) {
backends.set(backend.name, { backend: onnxruntimeBackend });
}
Comment thread
vkozyura marked this conversation as resolved.
const backendNames = [...backends.keys()];
// try to resolve and initialize all requested backends
let backend;
const errors = [];
const availableBackendNames = new Set();
for (const backendName of backendNames) {
// eslint-disable-next-line no-await-in-loop
const resolveResult = await tryResolveAndInitializeBackend(backendName, backends);
if (typeof resolveResult === 'string') {
errors.push({ name: backendName, err: resolveResult });
} else {
if (!backend) {
backend = resolveResult;
}
if (backend === resolveResult) {
availableBackendNames.add(backendName);
}
}
}
// if no backend is available, throw error.
if (!backend) {
throw new Error(
`no available backend found. ERR: ${errors.map((e) => `[${e.name}] ${e.err}`).join(', ')}`
);
}
return [
backend,
new Proxy(
{},
{
get: (target, prop) => {
if (prop === 'executionProviders') {
return [];
}
return Reflect.get(target, prop);
}
}
)
];
}

async function tryResolveAndInitializeBackend(backendName, backends) {
const backendInfo = backends.get(backendName);
if (!backendInfo) {
return 'backend not found.';
}
if (backendInfo.initialized) {
return backendInfo.backend;
} else if (backendInfo.aborted) {
return backendInfo.error;
} else {
const isInitializing = !!backendInfo.initPromise;
try {
if (!isInitializing) {
backendInfo.initPromise = backendInfo.backend.init(backendName);
}
await backendInfo.initPromise;
backendInfo.initialized = true;
return backendInfo.backend;
} catch (e) {
if (!isInitializing) {
backendInfo.error = `${e}`;
backendInfo.aborted = true;
}
return backendInfo.error;
} finally {
delete backendInfo.initPromise;
}
}
}

// Copy from test/bookshop/node_modules/onnxruntime-node/dist/backend.js
const dataTypeStrings = [
undefined,
'float32',
'uint8',
'int8',
'uint16',
'int16',
'int32',
'int64',
'string',
'bool',
'float16',
'float64',
'uint32',
'uint64',
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
'uint4',
'int4'
];
class OnnxruntimeSessionHandler {
static inferenceSession = new WeakMap();
constructor(pathOrBuffer, options) {
binding.initOrt();
OnnxruntimeSessionHandler.inferenceSession.set(this, new binding.binding.InferenceSession());
if (typeof pathOrBuffer === 'string') {
OnnxruntimeSessionHandler.inferenceSession.get(this).loadModel(pathOrBuffer, options);
} else {
OnnxruntimeSessionHandler.inferenceSession
.get(this)
.loadModel(pathOrBuffer.buffer, pathOrBuffer.byteOffset, pathOrBuffer.byteLength, options);
}
// prepare input/output names and metadata
this.inputNames = [];
this.outputNames = [];
this.inputMetadata = [];
this.outputMetadata = [];
// this function takes raw metadata from binding and returns a tuple of the following 2 items:
// - an array of string representing names
// - an array of converted InferenceSession.ValueMetadata
const fillNamesAndMetadata = (rawMetadata) => {
const names = [];
const metadata = [];
for (const m of rawMetadata) {
names.push(m.name);
if (!m.isTensor) {
metadata.push({ name: m.name, isTensor: false });
} else {
const type = dataTypeStrings[m.type];
if (type === undefined) {
throw new Error(`Unsupported data type: ${m.type}`);
}
const shape = [];
for (let i = 0; i < m.shape.length; ++i) {
const dim = m.shape[i];
if (dim === -1) {
shape.push(m.symbolicDimensions[i]);
} else if (dim >= 0) {
shape.push(dim);
} else {
throw new Error(`Invalid dimension: ${dim}`);
}
}
metadata.push({
name: m.name,
isTensor: m.isTensor,
type,
shape
});
}
}
return [names, metadata];
};
[this.inputNames, this.inputMetadata] = fillNamesAndMetadata(
OnnxruntimeSessionHandler.inferenceSession.get(this).inputMetadata
);
[this.outputNames, this.outputMetadata] = fillNamesAndMetadata(
OnnxruntimeSessionHandler.inferenceSession.get(this).outputMetadata
);
}
async dispose() {
OnnxruntimeSessionHandler.inferenceSession.get(this).dispose();
}
run(feeds, fetches, options) {
return OnnxruntimeSessionHandler.inferenceSession.get(this).run(feeds, fetches, options);
}
}
class OnnxruntimeBackend {
init() {}
createInferenceSessionHandler(pathOrBuffer, options) {
return new OnnxruntimeSessionHandler(pathOrBuffer, options || {});
}
}
Comment thread
vkozyura marked this conversation as resolved.
const onnxruntimeBackend = new OnnxruntimeBackend();
const listSupportedBackends = binding.binding.listSupportedBackends;

export { InferenceSession };
Loading
Loading