use std::sync::Arc;
use crate::core::sync::RwLock;
use napi::bindgen_prelude::*;
use napi_derive::napi;
use super::{napi_err, run_async};
use crate::core::embed::{EmbedKind, EmbedLimits, EmbedOptions, Embedder, Pooling, SemanticIndex};
fn checked_count(input: f64, minimum: u32, name: &str) -> anyhow::Result<usize> {
anyhow::ensure!(
input.is_finite()
&& input.fract() == 0.0
&& (f64::from(minimum)..=f64::from(u32::MAX)).contains(&input),
"{name} must be a finite integer in {minimum}..=4294967295"
);
Ok(input as usize)
}
#[napi(object)]
#[derive(Default)]
pub struct EmbedLimitsJs {
pub max_texts: Option<f64>,
pub max_input_bytes: Option<f64>,
pub max_tokens: Option<f64>,
pub max_output_bytes: Option<f64>,
pub max_concurrent_calls: Option<f64>,
}
impl TryFrom<EmbedLimitsJs> for EmbedLimits {
type Error = anyhow::Error;
fn try_from(value: EmbedLimitsJs) -> anyhow::Result<Self> {
let defaults = Self::default();
let limit = |input: Option<f64>, default, name| -> anyhow::Result<usize> {
let Some(input) = input else {
return Ok(default);
};
checked_count(input, 1, name)
};
Ok(Self {
max_texts: limit(value.max_texts, defaults.max_texts, "max_texts")?,
max_input_bytes: limit(
value.max_input_bytes,
defaults.max_input_bytes,
"max_input_bytes",
)?,
max_tokens: limit(value.max_tokens, defaults.max_tokens, "max_tokens")?,
max_output_bytes: limit(
value.max_output_bytes,
defaults.max_output_bytes,
"max_output_bytes",
)?,
max_concurrent_calls: limit(
value.max_concurrent_calls,
defaults.max_concurrent_calls,
"max_concurrent_calls",
)?,
})
}
}
#[napi(object)]
pub struct EmbeddingInputJs {
pub input_bytes: u32,
pub retained_bytes: u32,
pub tokens: u32,
pub byte_truncated: bool,
pub token_truncated: bool,
}
#[napi(object)]
pub struct EmbeddingBatchJs {
pub embeddings: Vec<Float32Array>,
pub inputs: Vec<EmbeddingInputJs>,
}
#[napi(object)]
#[derive(Default)]
pub struct EmbedOptionsJs {
pub model: Option<String>,
pub sha256: Option<String>,
pub max_length: Option<f64>,
pub pooling: Option<String>,
pub normalize: Option<bool>,
pub query_prefix: Option<String>,
pub passage_prefix: Option<String>,
pub batch_size: Option<f64>,
pub max_batch_tokens: Option<f64>,
pub limits: Option<EmbedLimitsJs>,
pub threads: Option<f64>,
pub offline: Option<bool>,
pub model_dir: Option<String>,
pub cache_dir: Option<String>,
}
impl EmbedOptionsJs {
fn into_options(self) -> anyhow::Result<EmbedOptions> {
let d = EmbedOptions::default();
Ok(EmbedOptions {
model: self.model.unwrap_or(d.model),
sha256: self.sha256,
max_length: self
.max_length
.map(|v| checked_count(v, 2, "max_length"))
.transpose()?,
pooling: self
.pooling
.as_deref()
.map(str::parse::<Pooling>)
.transpose()?,
normalize: self.normalize,
query_prefix: self.query_prefix,
passage_prefix: self.passage_prefix,
batch_size: self
.batch_size
.map(|v| checked_count(v, 1, "batch_size"))
.transpose()?
.unwrap_or(d.batch_size),
max_batch_tokens: self
.max_batch_tokens
.map(|v| checked_count(v, 1, "max_batch_tokens"))
.transpose()?
.unwrap_or(d.max_batch_tokens),
limits: self.limits.unwrap_or_default().try_into()?,
threads: self
.threads
.map(|v| checked_count(v, 0, "threads"))
.transpose()?
.unwrap_or(d.threads),
offline: self.offline,
model_dir: self.model_dir.map(Into::into),
cache_dir: self.cache_dir.map(Into::into),
})
}
}
async fn load(options: Option<EmbedOptionsJs>) -> Result<Arc<Embedder>> {
let options = options
.unwrap_or_default()
.into_options()
.map_err(napi_err)?;
run_async(move || Embedder::new(&options).map(Arc::new)).await
}
#[napi]
pub struct LocalEmbedder {
inner: Arc<Embedder>,
}
#[napi]
impl LocalEmbedder {
#[napi(factory)]
pub async fn load(options: Option<EmbedOptionsJs>) -> Result<Self> {
Ok(Self {
inner: load(options).await?,
})
}
#[napi]
pub async fn embed_query(&self, texts: Vec<String>) -> Result<Vec<Float32Array>> {
self.run(texts, EmbedKind::Query).await
}
#[napi]
pub async fn embed_passages(&self, texts: Vec<String>) -> Result<Vec<Float32Array>> {
self.run(texts, EmbedKind::Passage).await
}
#[napi]
pub async fn embed_query_with_report(&self, texts: Vec<String>) -> Result<EmbeddingBatchJs> {
self.run_with_report(texts, EmbedKind::Query).await
}
#[napi]
pub async fn embed_passages_with_report(&self, texts: Vec<String>) -> Result<EmbeddingBatchJs> {
self.run_with_report(texts, EmbedKind::Passage).await
}
#[napi(getter)]
pub fn dimensions(&self) -> u32 {
self.inner.dimensions() as u32
}
#[napi(getter)]
pub fn model(&self) -> String {
self.inner.model().into()
}
#[napi(getter)]
pub fn revision(&self) -> String {
self.inner.revision().into()
}
#[napi(getter)]
pub fn quantization(&self) -> String {
self.inner.quantization().name().into()
}
#[napi(getter)]
pub fn pack_sha256(&self) -> String {
self.inner.pack_sha256().into()
}
}
impl LocalEmbedder {
async fn run_with_report(
&self,
texts: Vec<String>,
kind: EmbedKind,
) -> Result<EmbeddingBatchJs> {
let embedder = self.inner.clone();
let batch = run_async(move || embedder.embed_with_report(&texts, kind)).await?;
let inputs = batch
.inputs
.into_iter()
.map(|input| {
Ok(EmbeddingInputJs {
input_bytes: u32::try_from(input.input_bytes)
.map_err(|e| napi_err(e.into()))?,
retained_bytes: u32::try_from(input.retained_bytes)
.map_err(|e| napi_err(e.into()))?,
tokens: u32::try_from(input.tokens).map_err(|e| napi_err(e.into()))?,
byte_truncated: input.byte_truncated,
token_truncated: input.token_truncated,
})
})
.collect::<Result<_>>()?;
Ok(EmbeddingBatchJs {
embeddings: batch
.embeddings
.into_iter()
.map(Float32Array::new)
.collect(),
inputs,
})
}
async fn run(&self, texts: Vec<String>, kind: EmbedKind) -> Result<Vec<Float32Array>> {
let embedder = self.inner.clone();
let rows = run_async(move || embedder.embed(&texts, kind)).await?;
Ok(rows.into_iter().map(Float32Array::new).collect())
}
}
#[napi(object)]
pub struct SemanticHit {
pub id: String,
pub score: f64,
pub text: String,
}
#[napi(js_name = "SemanticIndex")]
pub struct JsSemanticIndex {
inner: Arc<RwLock<SemanticIndex>>,
}
#[napi]
impl JsSemanticIndex {
#[napi(factory)]
pub fn create(embedder: &LocalEmbedder) -> Self {
Self {
inner: Arc::new(RwLock::new(SemanticIndex::new(embedder.inner.clone()))),
}
}
#[napi]
pub async fn add(&self, ids: Vec<String>, texts: Vec<String>) -> Result<()> {
let index = self.inner.clone();
run_async(move || index.write().add(&ids, &texts)).await
}
#[napi]
pub async fn search(&self, query: String, k: Option<u32>) -> Result<Vec<SemanticHit>> {
let index = self.inner.clone();
let hits = run_async(move || index.read().search(&query, k.unwrap_or(10) as usize)).await?;
Ok(hits
.into_iter()
.map(|h| SemanticHit {
id: h.id,
score: f64::from(h.score),
text: h.text,
})
.collect())
}
#[napi]
pub fn remove(&self, id: String) -> bool {
self.inner.write().remove(&id)
}
#[napi]
pub async fn save(&self, path: String) -> Result<()> {
let index = self.inner.clone();
run_async(move || index.read().save(std::path::Path::new(&path))).await
}
#[napi(factory)]
pub fn load(path: String, embedder: &LocalEmbedder) -> Result<Self> {
let index = SemanticIndex::load(std::path::Path::new(&path), embedder.inner.clone())
.map_err(napi_err)?;
Ok(Self {
inner: Arc::new(RwLock::new(index)),
})
}
#[napi(getter)]
pub fn length(&self) -> u32 {
self.inner.read().len() as u32
}
}