diff --git a/README.md b/README.md
index 1d1c69f..17b8b42 100644
--- a/README.md
+++ b/README.md
@@ -298,6 +298,82 @@ const App = () => {
};
```
+### Tokenization
+
+Convert text into tokens using the model's tokenizer.
+
+#### Class
+
+```typescript
+import { CactusLM } from 'cactus-react-native';
+
+const cactusLM = new CactusLM();
+
+const result = await cactusLM.tokenize({ text: 'Hello, World!' });
+console.log('Token IDs:', result.tokens);
+```
+
+#### Hook
+
+```tsx
+import { useCactusLM } from 'cactus-react-native';
+
+const App = () => {
+ const cactusLM = useCactusLM();
+
+ const handleTokenize = async () => {
+ const result = await cactusLM.tokenize({ text: 'Hello, World!' });
+ console.log('Token IDs:', result.tokens);
+ };
+
+ return ;
+};
+```
+
+### Score Window
+
+Calculate perplexity scores for a window of tokens within a sequence.
+
+#### Class
+
+```typescript
+import { CactusLM } from 'cactus-react-native';
+
+const cactusLM = new CactusLM();
+
+const tokens = [123, 456, 789, 101, 112];
+const result = await cactusLM.scoreWindow({
+ tokens,
+ start: 1,
+ end: 3,
+ context: 2
+});
+console.log('Score:', result.score);
+```
+
+#### Hook
+
+```tsx
+import { useCactusLM } from 'cactus-react-native';
+
+const App = () => {
+ const cactusLM = useCactusLM();
+
+ const handleScoreWindow = async () => {
+ const tokens = [123, 456, 789, 101, 112];
+ const result = await cactusLM.scoreWindow({
+ tokens,
+ start: 1,
+ end: 3,
+ context: 2
+ });
+ console.log('Score:', result.score);
+ };
+
+ return ;
+};
+```
+
### Embedding
Convert text and images into numerical vector representations that capture semantic meaning, useful for similarity search and semantic understanding.
@@ -423,7 +499,7 @@ The `CactusSTT` class provides audio transcription and audio embedding capabilit
### Transcription
-Transcribe audio files to text with streaming support.
+Transcribe audio to text with streaming support. Accepts either a file path or raw PCM audio samples.
#### Class
@@ -434,12 +510,22 @@ const cactusSTT = new CactusSTT({ model: 'whisper-small' });
await cactusSTT.init();
+// Transcribe from file path
const result = await cactusSTT.transcribe({
- audioFilePath: 'path/to/audio.wav',
+ audio: 'path/to/audio.wav',
onToken: (token) => console.log('Token:', token)
});
console.log('Transcription:', result.response);
+
+// Or transcribe from raw PCM samples
+const pcmSamples: number[] = [/* ... */];
+const result2 = await cactusSTT.transcribe({
+ audio: pcmSamples,
+ onToken: (token) => console.log('Token:', token)
+});
+
+console.log('Transcription:', result2.response);
```
#### Hook
@@ -451,10 +537,17 @@ const App = () => {
const cactusSTT = useCactusSTT({ model: 'whisper-small' });
const handleTranscribe = async () => {
+ // Transcribe from file path
const result = await cactusSTT.transcribe({
- audioFilePath: 'path/to/audio.wav',
+ audio: 'path/to/audio.wav',
});
console.log('Transcription:', result.response);
+
+ const pcmSamples: number[] = [/* ... */];
+ const result2 = await cactusSTT.transcribe({
+ audio: pcmSamples,
+ });
+ console.log('Transcription:', result2.response);
};
return (
@@ -507,6 +600,251 @@ const App = () => {
};
```
+## Vector Index
+
+The `CactusIndex` class provides a vector database for storing and querying embeddings with metadata. Enabling similarity search and retrieval.
+
+### Creating and Initializing an Index
+
+#### Class
+
+```typescript
+import { CactusIndex } from 'cactus-react-native';
+
+const cactusIndex = new CactusIndex('my-index', 1024);
+await cactusIndex.init();
+```
+
+#### Hook
+
+```tsx
+import { useCactusIndex } from 'cactus-react-native';
+
+const App = () => {
+ const cactusIndex = useCactusIndex({
+ name: 'my-index',
+ embeddingDim: 1024
+ });
+
+ const handleInit = async () => {
+ await cactusIndex.init();
+ };
+
+ return
+};
+```
+
+### Adding Documents
+
+Add documents with their embeddings and metadata to the index.
+
+#### Class
+
+```typescript
+import { CactusIndex } from 'cactus-react-native';
+
+const cactusIndex = new CactusIndex('my-index', 1024);
+await cactusIndex.init();
+
+await cactusIndex.add({
+ ids: [1, 2, 3],
+ documents: ['First document', 'Second document', 'Third document'],
+ embeddings: [
+ [0.1, 0.2, ...],
+ [0.3, 0.4, ...],
+ [0.5, 0.6, ...]
+ ],
+ metadatas: ['metadata1', 'metadata2', 'metadata3']
+});
+```
+
+#### Hook
+
+```tsx
+import { useCactusIndex } from 'cactus-react-native';
+
+const App = () => {
+ const cactusIndex = useCactusIndex({
+ name: 'my-index',
+ embeddingDim: 1024
+ });
+
+ const handleAdd = async () => {
+ await cactusIndex.add({
+ ids: [1, 2, 3],
+ documents: ['First document', 'Second document', 'Third document'],
+ embeddings: [
+ [0.1, 0.2, ...],
+ [0.3, 0.4, ...],
+ [0.5, 0.6, ...]
+ ],
+ metadatas: ['metadata1', 'metadata2', 'metadata3']
+ });
+ };
+
+ return ;
+};
+```
+
+### Querying the Index
+
+Search for similar documents using embedding vectors.
+
+#### Class
+
+```typescript
+import { CactusIndex } from 'cactus-react-native';
+
+const cactusIndex = new CactusIndex('my-index', 1024);
+await cactusIndex.init();
+
+const result = await cactusIndex.query({
+ embeddings: [[0.1, 0.2, ...]],
+ options: {
+ topK: 5,
+ scoreThreshold: 0.7
+ }
+});
+
+console.log('IDs:', result.ids);
+console.log('Scores:', result.scores);
+```
+
+#### Hook
+
+```tsx
+import { useCactusIndex } from 'cactus-react-native';
+
+const App = () => {
+ const cactusIndex = useCactusIndex({
+ name: 'my-index',
+ embeddingDim: 1024
+ });
+
+ const handleQuery = async () => {
+ const result = await cactusIndex.query({
+ embeddings: [[0.1, 0.2, ...]],
+ options: {
+ topK: 5,
+ scoreThreshold: 0.7
+ }
+ });
+ console.log('IDs:', result.ids);
+ console.log('Scores:', result.scores);
+ };
+
+ return ;
+};
+```
+
+### Retrieving Documents
+
+Get documents by their IDs.
+
+#### Class
+
+```typescript
+import { CactusIndex } from 'cactus-react-native';
+
+const cactusIndex = new CactusIndex('my-index', 1024);
+await cactusIndex.init();
+
+const result = await cactusIndex.get({ ids: [1, 2, 3] });
+console.log('Documents:', result.documents);
+console.log('Metadatas:', result.metadatas);
+console.log('Embeddings:', result.embeddings);
+```
+
+#### Hook
+
+```tsx
+import { useCactusIndex } from 'cactus-react-native';
+
+const App = () => {
+ const cactusIndex = useCactusIndex({
+ name: 'my-index',
+ embeddingDim: 1024
+ });
+
+ const handleGet = async () => {
+ const result = await cactusIndex.get({ ids: [1, 2, 3] });
+ console.log('Documents:', result.documents);
+ console.log('Metadatas:', result.metadatas);
+ console.log('Embeddings:', result.embeddings);
+ };
+
+ return ;
+};
+```
+
+### Deleting Documents
+
+Mark documents as deleted by their IDs.
+
+#### Class
+
+```typescript
+import { CactusIndex } from 'cactus-react-native';
+
+const cactusIndex = new CactusIndex('my-index', 1024);
+await cactusIndex.init();
+
+await cactusIndex.delete({ ids: [1, 2, 3] });
+```
+
+#### Hook
+
+```tsx
+import { useCactusIndex } from 'cactus-react-native';
+
+const App = () => {
+ const cactusIndex = useCactusIndex({
+ name: 'my-index',
+ embeddingDim: 1024
+ });
+
+ const handleDelete = async () => {
+ await cactusIndex.delete({ ids: [1, 2, 3] });
+ };
+
+ return ;
+};
+```
+
+### Compacting the Index
+
+Optimize the index by removing deleted documents and reorganizing data.
+
+#### Class
+
+```typescript
+import { CactusIndex } from 'cactus-react-native';
+
+const cactusIndex = new CactusIndex('my-index', 1024);
+await cactusIndex.init();
+
+await cactusIndex.compact();
+```
+
+#### Hook
+
+```tsx
+import { useCactusIndex } from 'cactus-react-native';
+
+const App = () => {
+ const cactusIndex = useCactusIndex({
+ name: 'my-index',
+ embeddingDim: 1024
+ });
+
+ const handleCompact = async () => {
+ await cactusIndex.compact();
+ };
+
+ return ;
+};
+```
+
## API Reference
### CactusLM Class
@@ -516,7 +854,7 @@ const App = () => {
**`new CactusLM(params?: CactusLMParams)`**
**Parameters:**
-- `model` - Model slug (default: `'qwen3-0.6'`).
+- `model` - Model slug or absolute path to Cactus model (default: `'qwen3-0.6'`).
- `contextSize` - Context window size (default: `2048`).
- `corpusDir` - Directory containing text files for RAG (default: `undefined`).
@@ -545,16 +883,35 @@ Performs text completion with optional streaming and tool support. Automatically
- `topK` - Top-K sampling limit (default: model-optimized).
- `maxTokens` - Maximum number of tokens to generate (default: `512`).
- `stopSequences` - Array of strings to stop generation (default: `undefined`).
+ - `forceTools` - Force the model to call one of the provided tools (default: `false`).
- `tools` - Array of `Tool` objects for function calling (default: `undefined`).
- `onToken` - Callback for streaming tokens.
- `mode` - Completion mode: `'local'` | `'hybrid'` (default: `'local'`)
+**`tokenize(params: CactusLMTokenizeParams): Promise`**
+
+Converts text into tokens using the model's tokenizer.
+
+**Parameters:**
+- `text` - Text to tokenize.
+
+**`scoreWindow(params: CactusLMScoreWindowParams): Promise`**
+
+Calculates perplexity scores for a window of tokens within a sequence.
+
+**Parameters:**
+- `tokens` - Array of token IDs.
+- `start` - Start index of the window.
+- `end` - End index of the window.
+- `context` - Number of context tokens before the window.
+
**`embed(params: CactusLMEmbedParams): Promise`**
Generates embeddings for the given text. Automatically calls `init()` if not already initialized. Throws an error if a generation (completion or embedding) is already in progress.
**Parameters:**
- `text` - Text to embed.
+- `normalize` - Whether to normalize the embedding vector (default: `false`).
**`imageEmbed(params: CactusLMImageEmbedParams): Promise`**
@@ -598,6 +955,8 @@ The `useCactusLM` hook manages a `CactusLM` instance with reactive state. When m
- `download(params?: CactusLMDownloadParams): Promise` - Downloads the model. Updates `isDownloading` and `downloadProgress` state during download. Sets `isDownloaded` to `true` on success.
- `init(): Promise` - Initializes the model for inference. Sets `isInitializing` to `true` during initialization.
- `complete(params: CactusLMCompleteParams): Promise` - Generates text completions. Automatically accumulates tokens in the `completion` state during streaming. Sets `isGenerating` to `true` while generating. Clears `completion` before starting.
+- `tokenize(params: CactusLMTokenizeParams): Promise` - Converts text into tokens. Sets `isGenerating` to `true` during operation.
+- `scoreWindow(params: CactusLMScoreWindowParams): Promise` - Calculates perplexity scores for a window of tokens. Sets `isGenerating` to `true` during operation.
- `embed(params: CactusLMEmbedParams): Promise` - Generates embeddings for the given text. Sets `isGenerating` to `true` during operation.
- `imageEmbed(params: CactusLMImageEmbedParams): Promise` - Generates embeddings for the given image. Sets `isGenerating` to `true` while generating.
- `stop(): Promise` - Stops ongoing generation. Clears any errors.
@@ -612,7 +971,7 @@ The `useCactusLM` hook manages a `CactusLM` instance with reactive state. When m
**`new CactusSTT(params?: CactusSTTParams)`**
**Parameters:**
-- `model` - Model slug (default: `'whisper-small'`).
+- `model` - Model slug or absolute path to Cactus model (default: `'qwen3-0.6'`).
- `contextSize` - Context window size (default: `2048`).
#### Methods
@@ -630,10 +989,10 @@ Initializes the model and prepares it for inference. Safe to call multiple times
**`transcribe(params: CactusSTTTranscribeParams): Promise`**
-Transcribes audio to text with optional streaming support. Automatically calls `init()` if not already initialized. Throws an error if a generation is already in progress.
+Transcribes audio to text with optional streaming support. Accepts either a file path or raw PCM audio samples. Automatically calls `init()` if not already initialized. Throws an error if a generation is already in progress.
**Parameters:**
-- `audioFilePath` - Path to the audio file.
+- `audio` - Path to the audio file or raw PCM samples.
- `prompt` - Optional prompt to guide transcription (default: `'<|startoftranscript|><|en|><|transcribe|><|notimestamps|>'`).
- `options` - Transcription options:
- `temperature` - Sampling temperature (default: model-optimized).
@@ -691,6 +1050,84 @@ The `useCactusSTT` hook manages a `CactusSTT` instance with reactive state. When
- `destroy(): Promise` - Releases all resources associated with the model. Clears the `transcription` state. Automatically called when the component unmounts.
- `getModels(): Promise` - Fetches available STT models from the database and checks their download status.
+### CactusIndex Class
+
+#### Constructor
+
+**`new CactusIndex(name: string, embeddingDim: number)`**
+
+**Parameters:**
+- `name` - Name of the index.
+- `embeddingDim` - Dimension of the embedding vectors.
+
+#### Methods
+
+**`init(): Promise`**
+
+Initializes the index and prepares it for operations. Must be called before using any other methods.
+
+**`add(params: CactusIndexAddParams): Promise`**
+
+Adds documents with their embeddings and metadata to the index.
+
+**Parameters:**
+- `ids` - Array of document IDs.
+- `documents` - Array of document texts.
+- `embeddings` - Array of embedding vectors (each vector must match `embeddingDim`).
+- `metadatas` - Optional array of metadata strings.
+
+**`query(params: CactusIndexQueryParams): Promise`**
+
+Searches for similar documents using embedding vectors.
+
+**Parameters:**
+- `embeddings` - Array of query embedding vectors.
+- `options` - Query options:
+ - `topK` - Number of top results to return (default: 10).
+ - `scoreThreshold` - Minimum similarity score threshold (default: -1.0).
+
+**`get(params: CactusIndexGetParams): Promise`**
+
+Retrieves documents by their IDs.
+
+**Parameters:**
+- `ids` - Array of document IDs to retrieve.
+
+**`delete(params: CactusIndexDeleteParams): Promise`**
+
+Deletes documents from the index by their IDs.
+
+**Parameters:**
+- `ids` - Array of document IDs to delete.
+
+**`compact(): Promise`**
+
+Optimizes the index by removing deleted documents and reorganizing data for better performance. Call after a series of deletions.
+
+**`destroy(): Promise`**
+
+Releases all resources associated with the index from memory.
+
+### useCactusIndex Hook
+
+The `useCactusIndex` hook manages a `CactusIndex` instance with reactive state. When index parameters (`name` or `embeddingDim`) change, the hook creates a new instance and resets all state. The hook automatically cleans up resources when the component unmounts.
+
+#### State
+
+- `isInitializing: boolean` - Whether the index is initializing.
+- `isProcessing: boolean` - Whether the index is processing an operation (add, query, get, delete, or compact).
+- `error: string | null` - Last error message from any operation, or `null` if there is no error. Cleared before starting new operations.
+
+#### Methods
+
+- `init(): Promise` - Initializes the index. Sets `isInitializing` to `true` during initialization.
+- `add(params: CactusIndexAddParams): Promise` - Adds documents to the index. Sets `isProcessing` to `true` during operation.
+- `query(params: CactusIndexQueryParams): Promise` - Searches for similar documents. Sets `isProcessing` to `true` during operation.
+- `get(params: CactusIndexGetParams): Promise` - Retrieves documents by IDs. Sets `isProcessing` to `true` during operation.
+- `delete(params: CactusIndexDeleteParams): Promise` - Deletes documents. Sets `isProcessing` to `true` during operation.
+- `compact(): Promise` - Optimizes the index. Sets `isProcessing` to `true` during operation.
+- `destroy(): Promise` - Releases all resources. Automatically called when the component unmounts.
+
## Type Definitions
### CactusLMParams
@@ -730,6 +1167,7 @@ interface CompleteOptions {
topK?: number;
maxTokens?: number;
stopSequences?: string[];
+ forceTools?: boolean;
}
```
@@ -783,11 +1221,47 @@ interface CactusLMCompleteResult {
}
```
+### CactusLMTokenizeParams
+
+```typescript
+interface CactusLMTokenizeParams {
+ text: string;
+}
+```
+
+### CactusLMTokenizeResult
+
+```typescript
+interface CactusLMTokenizeResult {
+ tokens: number[];
+}
+```
+
+### CactusLMScoreWindowParams
+
+```typescript
+interface CactusLMScoreWindowParams {
+ tokens: number[];
+ start: number;
+ end: number;
+ context: number;
+}
+```
+
+### CactusLMScoreWindowResult
+
+```typescript
+interface CactusLMScoreWindowResult {
+ score: number;
+}
+```
+
### CactusLMEmbedParams
```typescript
interface CactusLMEmbedParams {
text: string;
+ normalize?: boolean;
}
```
@@ -878,7 +1352,7 @@ interface TranscribeOptions {
```typescript
interface CactusSTTTranscribeParams {
- audioFilePath: string;
+ audio: string | number[];
prompt?: string;
options?: TranscribeOptions;
onToken?: (token: string) => void;
@@ -917,6 +1391,79 @@ interface CactusSTTAudioEmbedResult {
}
```
+### CactusIndexParams
+
+```typescript
+interface CactusIndexParams {
+ name: string;
+ embeddingDim: number;
+}
+```
+
+### CactusIndexAddParams
+
+```typescript
+interface CactusIndexAddParams {
+ ids: number[];
+ documents: string[];
+ embeddings: number[][];
+ metadatas?: string[];
+}
+```
+
+### CactusIndexGetParams
+
+```typescript
+interface CactusIndexGetParams {
+ ids: number[];
+}
+```
+
+### CactusIndexGetResult
+
+```typescript
+interface CactusIndexGetResult {
+ documents: string[];
+ metadatas: string[];
+ embeddings: number[][];
+}
+```
+
+### IndexQueryOptions
+
+```typescript
+interface IndexQueryOptions {
+ topK?: number;
+ scoreThreshold?: number;
+}
+```
+
+### CactusIndexQueryParams
+
+```typescript
+interface CactusIndexQueryParams {
+ embeddings: number[][];
+ options?: IndexQueryOptions;
+}
+```
+
+### CactusIndexQueryResult
+
+```typescript
+interface CactusIndexQueryResult {
+ ids: number[][];
+ scores: number[][];
+}
+```
+
+### CactusIndexDeleteParams
+
+```typescript
+interface CactusIndexDeleteParams {
+ ids: number[];
+}
+```
+
## Configuration
### Telemetry
diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt
index eaf9916..22b1cee 100644
--- a/android/CMakeLists.txt
+++ b/android/CMakeLists.txt
@@ -6,10 +6,11 @@ set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_STANDARD 20)
# Define C++ library and add all sources
-add_library(${PACKAGE_NAME} SHARED
+add_library(${PACKAGE_NAME} SHARED
src/main/cpp/cpp-adapter.cpp
../cpp/HybridCactus.cpp
../cpp/HybridCactusUtil.cpp
+ ../cpp/HybridCactusIndex.cpp
)
add_library(libcactus STATIC IMPORTED)
@@ -17,9 +18,9 @@ set_target_properties(libcactus PROPERTIES
IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/src/main/jniLibs/${ANDROID_ABI}/libcactus.a"
)
-add_library(libcactus_util SHARED IMPORTED)
+add_library(libcactus_util STATIC IMPORTED)
set_target_properties(libcactus_util PROPERTIES
- IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/src/main/jniLibs/${ANDROID_ABI}/libcactus_util.so"
+ IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/src/main/jniLibs/${ANDROID_ABI}/libcactus_util.a"
)
# Add Nitrogen specs :)
diff --git a/android/src/main/java/com/margelo/nitro/cactus/HybridCactusFileSystem.kt b/android/src/main/java/com/margelo/nitro/cactus/HybridCactusFileSystem.kt
index 3d7ae77..70b1d09 100644
--- a/android/src/main/java/com/margelo/nitro/cactus/HybridCactusFileSystem.kt
+++ b/android/src/main/java/com/margelo/nitro/cactus/HybridCactusFileSystem.kt
@@ -209,6 +209,8 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
modelFile.deleteRecursively()
}
+ override fun getIndexPath(name: String): Promise = Promise.async { indexFile(name).absolutePath }
+
private fun cactusFile(): File {
val cactusDir = File(context.filesDir, "cactus")
@@ -221,6 +223,23 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() {
private fun modelFile(model: String): File {
val cactusDir = cactusFile()
- return File(cactusDir, "models/$model")
+ val modelsDir = File(cactusDir, "models")
+
+ if (!modelsDir.exists()) {
+ modelsDir.mkdirs()
+ }
+
+ return File(modelsDir, model)
+ }
+
+ private fun indexFile(name: String): File {
+ val cactusDir = cactusFile()
+ val finalDir = File(cactusDir, "indexes/$name")
+
+ if (!finalDir.exists()) {
+ finalDir.mkdirs()
+ }
+
+ return finalDir
}
}
diff --git a/android/src/main/jniLibs/arm64-v8a/libcactus.a b/android/src/main/jniLibs/arm64-v8a/libcactus.a
index 60bf48f..0a72e9d 100644
Binary files a/android/src/main/jniLibs/arm64-v8a/libcactus.a and b/android/src/main/jniLibs/arm64-v8a/libcactus.a differ
diff --git a/android/src/main/jniLibs/arm64-v8a/libcactus_util.a b/android/src/main/jniLibs/arm64-v8a/libcactus_util.a
new file mode 100644
index 0000000..5db9dbe
Binary files /dev/null and b/android/src/main/jniLibs/arm64-v8a/libcactus_util.a differ
diff --git a/android/src/main/jniLibs/arm64-v8a/libcactus_util.so b/android/src/main/jniLibs/arm64-v8a/libcactus_util.so
deleted file mode 100755
index 9a5cad1..0000000
Binary files a/android/src/main/jniLibs/arm64-v8a/libcactus_util.so and /dev/null differ
diff --git a/cpp/HybridCactus.cpp b/cpp/HybridCactus.cpp
index ea9d913..7cce17a 100644
--- a/cpp/HybridCactus.cpp
+++ b/cpp/HybridCactus.cpp
@@ -1,6 +1,7 @@
#include "HybridCactus.hpp"
namespace margelo::nitro::cactus {
+
HybridCactus::HybridCactus() : HybridObject(TAG) {}
std::shared_ptr>
@@ -19,7 +20,8 @@ HybridCactus::init(const std::string &modelPath, double contextSize,
corpusDir ? corpusDir->c_str() : nullptr);
if (!model) {
- throw std::runtime_error("Failed to initialize Cactus model");
+ throw std::runtime_error("Cactus init failed: " +
+ std::string(cactus_get_last_error()));
}
this->_model = model;
@@ -65,7 +67,8 @@ std::shared_ptr> HybridCactus::complete(
cactusTokenCallback, &callbackCtx);
if (result < 0) {
- throw std::runtime_error("Cactus completion failed");
+ throw std::runtime_error("Cactus complete failed: " +
+ std::string(cactus_get_last_error()));
}
// Remove null terminator
@@ -75,12 +78,78 @@ std::shared_ptr> HybridCactus::complete(
});
}
+std::shared_ptr>>
+HybridCactus::tokenize(const std::string &text) {
+ return Promise>::async([this,
+ text]() -> std::vector {
+ std::lock_guard lock(this->_modelMutex);
+
+ if (!this->_model) {
+ throw std::runtime_error("Cactus model is not initialized");
+ }
+
+ std::vector tokenBuffer(text.length() * 2 + 16);
+ size_t outTokenLen = 0;
+
+ int result = cactus_tokenize(this->_model, text.c_str(), tokenBuffer.data(),
+ tokenBuffer.size(), &outTokenLen);
+
+ if (result < 0) {
+ throw std::runtime_error("Cactus tokenize failed: " +
+ std::string(cactus_get_last_error()));
+ }
+
+ tokenBuffer.resize(outTokenLen);
+
+ return std::vector(tokenBuffer.begin(), tokenBuffer.end());
+ });
+}
+
+std::shared_ptr>
+HybridCactus::scoreWindow(const std::vector &tokens, double start,
+ double end, double context) {
+ return Promise::async(
+ [this, tokens, start, end, context]() -> std::string {
+ std::lock_guard lock(this->_modelMutex);
+
+ if (!this->_model) {
+ throw std::runtime_error("Cactus model is not initialized");
+ }
+
+ std::vector tokenBuffer;
+ tokenBuffer.reserve(tokens.size());
+ for (double d : tokens) {
+ tokenBuffer.emplace_back(static_cast(d));
+ }
+
+ std::string responseBuffer;
+ responseBuffer.resize(1024);
+
+ int result = cactus_score_window(
+ this->_model, tokenBuffer.data(), tokenBuffer.size(),
+ static_cast(start), static_cast(end),
+ static_cast(context), responseBuffer.data(),
+ responseBuffer.size());
+
+ if (result < 0) {
+ throw std::runtime_error("Cactus score window failed: " +
+ std::string(cactus_get_last_error()));
+ }
+
+ // Remove null terminator
+ responseBuffer.resize(strlen(responseBuffer.c_str()));
+
+ return responseBuffer;
+ });
+}
+
std::shared_ptr> HybridCactus::transcribe(
- const std::string &audioFilePath, const std::string &prompt,
- double responseBufferSize, const std::optional &optionsJson,
+ const std::variant, std::string> &audio,
+ const std::string &prompt, double responseBufferSize,
+ const std::optional &optionsJson,
const std::optional> &callback) {
- return Promise::async([this, audioFilePath, prompt, optionsJson,
+ return Promise::async([this, audio, prompt, optionsJson,
callback,
responseBufferSize]() -> std::string {
std::lock_guard lock(this->_modelMutex);
@@ -105,14 +174,34 @@ std::shared_ptr> HybridCactus::transcribe(
std::string responseBuffer;
responseBuffer.resize(responseBufferSize);
- int result =
- cactus_transcribe(this->_model, audioFilePath.c_str(), prompt.c_str(),
- responseBuffer.data(), responseBufferSize,
- optionsJson ? optionsJson->c_str() : nullptr,
- cactusTokenCallback, &callbackCtx);
+ int result;
+ if (std::holds_alternative(audio)) {
+ result = cactus_transcribe(
+ this->_model, std::get(audio).c_str(), prompt.c_str(),
+ responseBuffer.data(), responseBufferSize,
+ optionsJson ? optionsJson->c_str() : nullptr, cactusTokenCallback,
+ &callbackCtx, nullptr, 0);
+ } else {
+ const auto &audioDoubles = std::get>(audio);
+
+ std::vector audioBytes;
+ audioBytes.reserve(audioDoubles.size());
+
+ for (double d : audioDoubles) {
+ d = std::clamp(d, 0.0, 255.0);
+ audioBytes.emplace_back(static_cast(d));
+ }
+
+ result = cactus_transcribe(this->_model, nullptr, prompt.c_str(),
+ responseBuffer.data(), responseBufferSize,
+ optionsJson ? optionsJson->c_str() : nullptr,
+ cactusTokenCallback, &callbackCtx,
+ audioBytes.data(), audioBytes.size());
+ }
if (result < 0) {
- throw std::runtime_error("Cactus transcription failed");
+ throw std::runtime_error("Cactus transcribe failed: " +
+ std::string(cactus_get_last_error()));
}
// Remove null terminator
@@ -123,9 +212,10 @@ std::shared_ptr> HybridCactus::transcribe(
}
std::shared_ptr>>
-HybridCactus::embed(const std::string &text, double embeddingBufferSize) {
+HybridCactus::embed(const std::string &text, double embeddingBufferSize,
+ bool normalize) {
return Promise>::async(
- [this, text, embeddingBufferSize]() -> std::vector {
+ [this, text, embeddingBufferSize, normalize]() -> std::vector {
std::lock_guard lock(this->_modelMutex);
if (!this->_model) {
@@ -135,12 +225,13 @@ HybridCactus::embed(const std::string &text, double embeddingBufferSize) {
std::vector embeddingBuffer(embeddingBufferSize);
size_t embeddingDim;
- int result =
- cactus_embed(this->_model, text.c_str(), embeddingBuffer.data(),
- embeddingBufferSize * sizeof(float), &embeddingDim);
+ int result = cactus_embed(
+ this->_model, text.c_str(), embeddingBuffer.data(),
+ embeddingBufferSize * sizeof(float), &embeddingDim, normalize);
if (result < 0) {
- throw std::runtime_error("Cactus embedding failed");
+ throw std::runtime_error("Cactus embed failed: " +
+ std::string(cactus_get_last_error()));
}
embeddingBuffer.resize(embeddingDim);
@@ -169,7 +260,8 @@ HybridCactus::imageEmbed(const std::string &imagePath,
embeddingBufferSize * sizeof(float), &embeddingDim);
if (result < 0) {
- throw std::runtime_error("Cactus image embedding failed");
+ throw std::runtime_error("Cactus image embed failed: " +
+ std::string(cactus_get_last_error()));
}
embeddingBuffer.resize(embeddingDim);
@@ -198,7 +290,8 @@ HybridCactus::audioEmbed(const std::string &audioPath,
embeddingBufferSize * sizeof(float), &embeddingDim);
if (result < 0) {
- throw std::runtime_error("Cactus audio embedding failed");
+ throw std::runtime_error("Cactus audio embed failed: " +
+ std::string(cactus_get_last_error()));
}
embeddingBuffer.resize(embeddingDim);
diff --git a/cpp/HybridCactus.hpp b/cpp/HybridCactus.hpp
index 575294d..fd49a1e 100644
--- a/cpp/HybridCactus.hpp
+++ b/cpp/HybridCactus.hpp
@@ -23,15 +23,24 @@ class HybridCactus : public HybridCactusSpec {
double /* tokenId */)>> &callback)
override;
+ std::shared_ptr>>
+ tokenize(const std::string &text) override;
+
+ std::shared_ptr>
+ scoreWindow(const std::vector &tokens, double start, double end,
+ double context) override;
+
std::shared_ptr> transcribe(
- const std::string &audioFilePath, const std::string &prompt,
- double responseBufferSize, const std::optional &optionsJson,
+ const std::variant, std::string> &audio,
+ const std::string &prompt, double responseBufferSize,
+ const std::optional &optionsJson,
const std::optional> &callback)
override;
std::shared_ptr>>
- embed(const std::string &text, double embeddingBufferSize) override;
+ embed(const std::string &text, double embeddingBufferSize,
+ bool normalize) override;
std::shared_ptr>>
imageEmbed(const std::string &imagePath, double embeddingBufferSize) override;
diff --git a/cpp/HybridCactusIndex.cpp b/cpp/HybridCactusIndex.cpp
new file mode 100644
index 0000000..b2da9ac
--- /dev/null
+++ b/cpp/HybridCactusIndex.cpp
@@ -0,0 +1,325 @@
+#include "HybridCactusIndex.hpp"
+
+namespace margelo::nitro::cactus {
+
+HybridCactusIndex::HybridCactusIndex() : HybridObject(TAG) {}
+
+std::shared_ptr>
+HybridCactusIndex::init(const std::string &indexPath, double embeddingDim) {
+ return Promise::async([this, indexPath, embeddingDim]() -> void {
+ std::lock_guard lock(this->_indexMutex);
+
+ if (this->_index) {
+ throw std::runtime_error("Cactus index is already initialized");
+ }
+
+ const cactus_index_t index =
+ cactus_index_init(indexPath.c_str(), embeddingDim);
+
+ if (!index) {
+ throw std::runtime_error("Cactus index init failed: " +
+ std::string(cactus_get_last_error()));
+ }
+
+ this->_index = index;
+ this->_embeddingDim = static_cast(embeddingDim);
+ });
+}
+
+std::shared_ptr> HybridCactusIndex::add(
+ const std::vector &ids, const std::vector &documents,
+ const std::vector> &embeddings,
+ const std::optional> &metadatas) {
+ return Promise::async([this, ids, documents, embeddings,
+ metadatas]() -> void {
+ std::lock_guard lock(this->_indexMutex);
+
+ if (!this->_index) {
+ throw std::runtime_error("Cactus index is not initialized");
+ }
+
+ const size_t count = ids.size();
+ if (documents.size() != count || embeddings.size() != count) {
+ throw std::runtime_error(
+ "ids, documents, and embeddings must have the same length");
+ }
+
+ if (metadatas.has_value() && metadatas->size() != count) {
+ throw std::runtime_error(
+ "metadatas must have the same length as other vectors");
+ }
+
+ std::vector intIds;
+ intIds.reserve(count);
+ for (size_t i = 0; i < count; ++i) {
+ intIds.emplace_back(static_cast(ids[i]));
+ }
+
+ std::vector documentPtrs;
+ documentPtrs.reserve(count);
+ for (size_t i = 0; i < count; ++i) {
+ documentPtrs.emplace_back(documents[i].c_str());
+ }
+
+ std::vector> embeddingsFloat(count);
+ std::vector embeddingPtrs;
+ embeddingPtrs.reserve(count);
+ for (size_t i = 0; i < count; ++i) {
+ embeddingsFloat[i].resize(embeddings[i].size());
+ for (size_t j = 0; j < embeddings[i].size(); ++j) {
+ embeddingsFloat[i][j] = static_cast(embeddings[i][j]);
+ }
+ embeddingPtrs.emplace_back(embeddingsFloat[i].data());
+ }
+
+ int result;
+ if (metadatas.has_value()) {
+ std::vector metadataPtrs;
+ metadataPtrs.reserve(count);
+ for (size_t i = 0; i < count; ++i) {
+ metadataPtrs.emplace_back((*metadatas)[i].c_str());
+ }
+ result = cactus_index_add(
+ this->_index, intIds.data(), documentPtrs.data(), metadataPtrs.data(),
+ embeddingPtrs.data(), count, this->_embeddingDim);
+ } else {
+ result = cactus_index_add(
+ this->_index, intIds.data(), documentPtrs.data(), nullptr,
+ embeddingPtrs.data(), count, this->_embeddingDim);
+ }
+
+ if (result < 0) {
+ throw std::runtime_error("Cactus index add failed: " +
+ std::string(cactus_get_last_error()));
+ }
+ });
+}
+
+std::shared_ptr>
+HybridCactusIndex::_delete(const std::vector &ids) {
+ return Promise::async([this, ids]() -> void {
+ std::lock_guard lock(this->_indexMutex);
+
+ if (!this->_index) {
+ throw std::runtime_error("Cactus index is not initialized");
+ }
+
+ std::vector intIds;
+ intIds.reserve(ids.size());
+ for (size_t i = 0; i < ids.size(); ++i) {
+ intIds.emplace_back(static_cast(ids[i]));
+ }
+
+ int result = cactus_index_delete(this->_index, intIds.data(), ids.size());
+
+ if (result < 0) {
+ throw std::runtime_error("Cactus index delete failed: " +
+ std::string(cactus_get_last_error()));
+ }
+ });
+}
+
+std::shared_ptr>
+HybridCactusIndex::get(const std::vector &ids) {
+ return Promise::async([this,
+ ids]() -> CactusIndexGetResult {
+ std::lock_guard lock(this->_indexMutex);
+
+ if (!this->_index) {
+ throw std::runtime_error("Cactus index is not initialized");
+ }
+
+ const size_t count = ids.size();
+
+ std::vector intIds;
+ intIds.reserve(count);
+ for (size_t i = 0; i < count; ++i) {
+ intIds.emplace_back(static_cast(ids[i]));
+ }
+
+ std::vector> documentBuffers;
+ documentBuffers.reserve(count);
+ std::vector> metadataBuffers;
+ metadataBuffers.reserve(count);
+ std::vector> embeddingBuffers;
+ embeddingBuffers.reserve(count);
+
+ const size_t maxStringSize = 65535;
+ std::vector documentBufferSizes(count, maxStringSize);
+ std::vector metadataBufferSizes(count, maxStringSize);
+ std::vector embeddingBufferSizes(count, this->_embeddingDim);
+
+ std::vector documentPtrs;
+ documentPtrs.reserve(count);
+ std::vector metadataPtrs;
+ metadataPtrs.reserve(count);
+ std::vector embeddingPtrs;
+ embeddingPtrs.reserve(count);
+
+ for (size_t i = 0; i < count; ++i) {
+ documentBuffers.emplace_back(std::make_unique(maxStringSize));
+ documentPtrs.emplace_back(documentBuffers[i].get());
+
+ metadataBuffers.emplace_back(std::make_unique(maxStringSize));
+ metadataPtrs.emplace_back(metadataBuffers[i].get());
+
+ embeddingBuffers.emplace_back(
+ std::make_unique(this->_embeddingDim));
+ embeddingPtrs.emplace_back(embeddingBuffers[i].get());
+ }
+
+ int result =
+ cactus_index_get(this->_index, intIds.data(), count,
+ documentPtrs.data(), documentBufferSizes.data(),
+ metadataPtrs.data(), metadataBufferSizes.data(),
+ embeddingPtrs.data(), embeddingBufferSizes.data());
+
+ if (result < 0) {
+ throw std::runtime_error("Cactus index get failed: " +
+ std::string(cactus_get_last_error()));
+ }
+
+ CactusIndexGetResult resultObj;
+ resultObj.documents.reserve(count);
+ resultObj.metadatas.reserve(count);
+ resultObj.embeddings = std::vector>(count);
+
+ for (size_t i = 0; i < count; ++i) {
+ resultObj.documents.emplace_back(std::string(documentBuffers[i].get()));
+ resultObj.metadatas.emplace_back(std::string(metadataBuffers[i].get()));
+
+ resultObj.embeddings[i].reserve(this->_embeddingDim);
+ for (size_t j = 0; j < this->_embeddingDim; ++j) {
+ resultObj.embeddings[i].emplace_back(
+ static_cast(embeddingBuffers[i].get()[j]));
+ }
+ }
+
+ return resultObj;
+ });
+}
+
+std::shared_ptr>
+HybridCactusIndex::query(const std::vector> &embeddings,
+ const std::optional &optionsJson) {
+ return Promise::async(
+ [this, embeddings, optionsJson]() -> CactusIndexQueryResult {
+ std::lock_guard lock(this->_indexMutex);
+
+ if (!this->_index) {
+ throw std::runtime_error("Cactus index is not initialized");
+ }
+
+ const size_t count = embeddings.size();
+
+ std::vector> embeddingsFloat(count);
+ std::vector embeddingPtrs;
+ embeddingPtrs.reserve(count);
+ for (size_t i = 0; i < count; ++i) {
+ embeddingsFloat[i].resize(embeddings[i].size());
+ for (size_t j = 0; j < embeddings[i].size(); ++j) {
+ embeddingsFloat[i][j] = static_cast(embeddings[i][j]);
+ }
+ embeddingPtrs.emplace_back(embeddingsFloat[i].data());
+ }
+
+ size_t maxResults = 10;
+ if (optionsJson.has_value()) {
+ const std::string &json = *optionsJson;
+ size_t pos = json.find("\"top_k\"");
+ if (pos != std::string::npos) {
+ size_t colonPos = json.find(':', pos);
+ if (colonPos != std::string::npos) {
+ size_t numStart = json.find_first_of("0123456789", colonPos);
+ if (numStart != std::string::npos) {
+ size_t numEnd = json.find_first_not_of("0123456789", numStart);
+ std::string numStr = json.substr(numStart, numEnd - numStart);
+ maxResults = std::stoul(numStr);
+ }
+ }
+ }
+ }
+
+ std::vector idBufferSizes(count, maxResults);
+ std::vector scoreBufferSizes(count, maxResults);
+
+ std::vector> idBuffers;
+ idBuffers.reserve(count);
+ std::vector> scoreBuffers;
+ scoreBuffers.reserve(count);
+
+ std::vector idPtrs;
+ idPtrs.reserve(count);
+ std::vector scorePtrs;
+ scorePtrs.reserve(count);
+
+ for (size_t i = 0; i < count; ++i) {
+ idBuffers.emplace_back(std::make_unique(maxResults));
+ idPtrs.emplace_back(idBuffers[i].get());
+
+ scoreBuffers.emplace_back(std::make_unique(maxResults));
+ scorePtrs.emplace_back(scoreBuffers[i].get());
+ }
+
+ int result = cactus_index_query(
+ this->_index, embeddingPtrs.data(), count, this->_embeddingDim,
+ optionsJson ? optionsJson->c_str() : nullptr, idPtrs.data(),
+ idBufferSizes.data(), scorePtrs.data(), scoreBufferSizes.data());
+
+ if (result < 0) {
+ throw std::runtime_error("Cactus index query failed: " +
+ std::string(cactus_get_last_error()));
+ }
+
+ CactusIndexQueryResult resultObj;
+ resultObj.ids = std::vector>(count);
+ resultObj.scores = std::vector>(count);
+
+ for (size_t i = 0; i < count; ++i) {
+ const size_t resultCount = idBufferSizes[i];
+ resultObj.ids[i].reserve(resultCount);
+ resultObj.scores[i].reserve(resultCount);
+
+ for (size_t j = 0; j < resultCount; ++j) {
+ resultObj.ids[i].emplace_back(
+ static_cast(idBuffers[i].get()[j]));
+ resultObj.scores[i].emplace_back(
+ static_cast(scoreBuffers[i].get()[j]));
+ }
+ }
+
+ return resultObj;
+ });
+}
+
+std::shared_ptr> HybridCactusIndex::compact() {
+ return Promise::async([this]() -> void {
+ std::lock_guard lock(this->_indexMutex);
+
+ if (!this->_index) {
+ throw std::runtime_error("Cactus index is not initialized");
+ }
+
+ int result = cactus_index_compact(this->_index);
+
+ if (result < 0) {
+ throw std::runtime_error("Cactus index compact failed: " +
+ std::string(cactus_get_last_error()));
+ }
+ });
+}
+
+std::shared_ptr> HybridCactusIndex::destroy() {
+ return Promise::async([this]() -> void {
+ std::lock_guard lock(this->_indexMutex);
+
+ if (!this->_index) {
+ throw std::runtime_error("Cactus index is not initialized");
+ }
+
+ cactus_index_destroy(this->_index);
+ this->_index = nullptr;
+ });
+}
+
+} // namespace margelo::nitro::cactus
diff --git a/cpp/HybridCactusIndex.hpp b/cpp/HybridCactusIndex.hpp
new file mode 100644
index 0000000..c423d32
--- /dev/null
+++ b/cpp/HybridCactusIndex.hpp
@@ -0,0 +1,43 @@
+#pragma once
+#include "HybridCactusIndexSpec.hpp"
+
+#include "cactus_ffi.h"
+
+#include
+
+namespace margelo::nitro::cactus {
+
+class HybridCactusIndex : public HybridCactusIndexSpec {
+public:
+ HybridCactusIndex();
+
+ std::shared_ptr> init(const std::string &indexPath,
+ double embeddingDim) override;
+
+ std::shared_ptr>
+ add(const std::vector &ids, const std::vector &documents,
+ const std::vector> &embeddings,
+ const std::optional> &metadatas) override;
+
+ std::shared_ptr>
+ _delete(const std::vector &ids) override;
+
+ std::shared_ptr>
+ get(const std::vector &ids) override;
+
+ std::shared_ptr>
+ query(const std::vector> &embeddings,
+ const std::optional &optionsJson) override;
+
+ std::shared_ptr> compact() override;
+
+ std::shared_ptr> destroy() override;
+
+private:
+ cactus_index_t _index = nullptr;
+ size_t _embeddingDim;
+
+ std::mutex _indexMutex;
+};
+
+} // namespace margelo::nitro::cactus
diff --git a/cpp/HybridCactusUtil.cpp b/cpp/HybridCactusUtil.cpp
index a2da907..7ebc083 100644
--- a/cpp/HybridCactusUtil.cpp
+++ b/cpp/HybridCactusUtil.cpp
@@ -23,12 +23,12 @@ HybridCactusUtil::registerApp(const std::string &encryptedData) {
}
std::shared_ptr>>
-HybridCactusUtil::getDeviceId() {
+HybridCactusUtil::getDeviceId(const std::optional &token) {
return Promise>::async(
- [this]() -> std::optional {
+ [this, token]() -> std::optional {
std::lock_guard lock(this->_mutex);
- const char *deviceId = get_device_id();
+ const char *deviceId = get_device_id(token ? token->c_str() : nullptr);
return deviceId ? std::optional(deviceId) : std::nullopt;
});
}
diff --git a/cpp/HybridCactusUtil.hpp b/cpp/HybridCactusUtil.hpp
index 6f72fa4..f84b8f2 100644
--- a/cpp/HybridCactusUtil.hpp
+++ b/cpp/HybridCactusUtil.hpp
@@ -14,7 +14,8 @@ class HybridCactusUtil : public HybridCactusUtilSpec {
std::shared_ptr>
registerApp(const std::string &encryptedData) override;
- std::shared_ptr>> getDeviceId() override;
+ std::shared_ptr>>
+ getDeviceId(const std::optional &token) override;
std::shared_ptr>
setAndroidDataDirectory(const std::string &dataDir) override;
diff --git a/cpp/cactus_ffi.h b/cpp/cactus_ffi.h
index 6bb3f27..e00b391 100644
--- a/cpp/cactus_ffi.h
+++ b/cpp/cactus_ffi.h
@@ -3,6 +3,7 @@
#include
#include
+#include
#if __GNUC__ >= 4
#define CACTUS_FFI_EXPORT __attribute__ ((visibility ("default")))
@@ -33,6 +34,26 @@ CACTUS_FFI_EXPORT int cactus_complete(
void* user_data
);
+CACTUS_FFI_EXPORT int cactus_tokenize(
+ cactus_model_t model,
+ const char* text,
+ uint32_t* token_buffer,
+ size_t token_buffer_len,
+ size_t* out_token_len
+);
+
+CACTUS_FFI_EXPORT int cactus_score_window(
+ cactus_model_t model,
+ const uint32_t* tokens,
+ size_t token_len,
+ size_t start,
+ size_t end,
+ size_t context,
+ char* response_buffer,
+ size_t buffer_size
+);
+
+
CACTUS_FFI_EXPORT int cactus_transcribe(
cactus_model_t model,
const char* audio_file_path,
@@ -41,7 +62,9 @@ CACTUS_FFI_EXPORT int cactus_transcribe(
size_t buffer_size,
const char* options_json,
cactus_token_callback callback,
- void* user_data
+ void* user_data,
+ const uint8_t* pcm_buffer,
+ size_t pcm_buffer_size
);
@@ -50,7 +73,8 @@ CACTUS_FFI_EXPORT int cactus_embed(
const char* text,
float* embeddings_buffer,
size_t buffer_size,
- size_t* embedding_dim
+ size_t* embedding_dim,
+ bool normalize
);
CACTUS_FFI_EXPORT int cactus_image_embed(
@@ -75,6 +99,63 @@ CACTUS_FFI_EXPORT void cactus_stop(cactus_model_t model);
CACTUS_FFI_EXPORT void cactus_destroy(cactus_model_t model);
+CACTUS_FFI_EXPORT const char* cactus_get_last_error(void);
+
+CACTUS_FFI_EXPORT void cactus_set_telemetry_token(const char* token);
+
+CACTUS_FFI_EXPORT void cactus_set_pro_key(const char* pro_key);
+
+typedef void* cactus_index_t;
+
+CACTUS_FFI_EXPORT cactus_index_t cactus_index_init(
+ const char* index_dir,
+ size_t embedding_dim
+);
+
+CACTUS_FFI_EXPORT int cactus_index_add(
+ cactus_index_t index,
+ const int* ids,
+ const char** documents,
+ const char** metadatas,
+ const float** embeddings,
+ size_t count,
+ size_t embedding_dim
+);
+
+CACTUS_FFI_EXPORT int cactus_index_delete(
+ cactus_index_t index,
+ const int* ids,
+ size_t ids_count
+);
+
+CACTUS_FFI_EXPORT int cactus_index_get(
+ cactus_index_t index,
+ const int* ids,
+ size_t ids_count,
+ char** document_buffers,
+ size_t* document_buffer_sizes,
+ char** metadata_buffers,
+ size_t* metadata_buffer_sizes,
+ float** embedding_buffers,
+ size_t* embedding_buffer_sizes
+);
+
+CACTUS_FFI_EXPORT int cactus_index_query(
+ cactus_index_t index,
+ const float** embeddings,
+ size_t embeddings_count,
+ size_t embedding_dim,
+ const char* options_json,
+ int** id_buffers,
+ size_t* id_buffer_sizes,
+ float** score_buffers,
+ size_t* score_buffer_sizes
+);
+
+CACTUS_FFI_EXPORT int cactus_index_compact(cactus_index_t index);
+
+CACTUS_FFI_EXPORT void cactus_index_destroy(cactus_index_t index);
+
#ifdef __cplusplus
}
#endif
diff --git a/cpp/cactus_util.h b/cpp/cactus_util.h
index 8ee154a..aa19ef0 100644
--- a/cpp/cactus_util.h
+++ b/cpp/cactus_util.h
@@ -7,7 +7,7 @@ extern "C" {
const char* register_app(const char* encrypted_data);
-const char* get_device_id();
+const char* get_device_id(const char* current_token);
// Helper function to free memory allocated by register_app
void free_string(const char* str);
diff --git a/example/Gemfile.lock b/example/Gemfile.lock
index d3116fb..ce4f4a3 100644
--- a/example/Gemfile.lock
+++ b/example/Gemfile.lock
@@ -1,25 +1,21 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (3.0.7)
- base64
- nkf
- rexml
+ CFPropertyList (3.0.9)
activesupport (6.1.7.10)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
- addressable (2.8.7)
- public_suffix (>= 2.0.2, < 7.0)
+ addressable (2.8.8)
+ public_suffix (>= 2.0.2, < 8.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
- base64 (0.3.0)
benchmark (0.5.0)
- bigdecimal (3.3.1)
+ bigdecimal (4.0.1)
claide (1.1.0)
cocoapods (1.15.2)
addressable (~> 2.8)
@@ -69,7 +65,7 @@ GEM
gh_inspector (1.1.3)
httpclient (2.9.0)
mutex_m
- i18n (1.14.7)
+ i18n (1.14.8)
concurrent-ruby (~> 1.0)
json (2.7.6)
logger (1.7.0)
@@ -79,7 +75,6 @@ GEM
nanaimo (0.3.0)
nap (1.1.0)
netrc (0.11.0)
- nkf (0.2.0)
public_suffix (4.0.7)
rexml (3.4.4)
ruby-macho (2.5.1)
@@ -113,4 +108,4 @@ RUBY VERSION
ruby 2.6.10p210
BUNDLED WITH
- 1.17.2
+ 2.4.22
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index f900f1d..f1bbe42 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,6 +1,6 @@
PODS:
- boost (1.84.0)
- - Cactus (1.2.1):
+ - Cactus (1.4.0):
- boost
- DoubleConversion
- fast_float
@@ -2643,7 +2643,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
- Cactus: 9eae9838fd1c7be78375534369f7b07123ae645c
+ Cactus: 83c36f3d76eb2102a79020b41201a3aae8b71956
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
FBLazyVector: b8f1312d48447cca7b4abc21ed155db14742bd03
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 63e6ea2..0033107 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -13,7 +13,7 @@ import ToolCallingScreen from './ToolCallingScreen';
import RAGScreen from './RAGScreen';
import STTScreen from './STTScreen';
import ChatScreen from './ChatScreen';
-import PerformanceScreen from './PerformanceScreen';
+import IndexScreen from './IndexScreen';
type Screen =
| 'Home'
@@ -23,7 +23,7 @@ type Screen =
| 'RAG'
| 'STT'
| 'Chat'
- | 'Performance';
+ | 'Index';
const App = () => {
const [selectedScreen, setSelectedScreen] = useState('Home');
@@ -56,8 +56,8 @@ const App = () => {
setSelectedScreen('Chat');
};
- const handleGoToPerformance = () => {
- setSelectedScreen('Performance');
+ const handleGoToIndex = () => {
+ setSelectedScreen('Index');
};
const renderScreen = () => {
@@ -74,8 +74,8 @@ const App = () => {
return ;
case 'Chat':
return ;
- case 'Performance':
- return ;
+ case 'Index':
+ return ;
default:
return null;
}
@@ -149,13 +149,10 @@ const App = () => {
-
- Performance
+
+ Vector Index
- Direct CactusLM class usage
+ CactusIndex with embeddings
diff --git a/example/src/IndexScreen.tsx b/example/src/IndexScreen.tsx
new file mode 100644
index 0000000..e10af8e
--- /dev/null
+++ b/example/src/IndexScreen.tsx
@@ -0,0 +1,384 @@
+import { useEffect, useState } from 'react';
+import {
+ View,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ ScrollView,
+ StyleSheet,
+ ActivityIndicator,
+} from 'react-native';
+import {
+ useCactusLM,
+ useCactusIndex,
+ type CactusIndexQueryResult,
+ type CactusIndexGetResult,
+} from 'cactus-react-native';
+
+const SAMPLE_DOCUMENTS = [
+ 'The capital of France is Paris.',
+ 'The largest planet in our solar system is Jupiter.',
+ 'The chemical symbol for water is H2O.',
+];
+
+const IndexScreen = () => {
+ const cactusLM = useCactusLM({
+ model: 'lfm2-350m',
+ });
+ const cactusIndex = useCactusIndex({
+ name: 'example_index',
+ embeddingDim: 1024,
+ });
+
+ // State for adding new document
+ const [newId, setNewId] = useState('');
+ const [newDoc, setNewDoc] = useState('');
+ const [newMetadata, setNewMetadata] = useState('');
+
+ // State for querying
+ const [query, setQuery] = useState('What is the capital of France?');
+ const [queryResults, setQueryResults] =
+ useState(null);
+ const [getResults, setGetResults] = useState(
+ null
+ );
+
+ useEffect(() => {
+ cactusLM.download();
+
+ // Cleanup on unmount
+ return () => {
+ cactusLM.destroy();
+ cactusIndex.destroy();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (!cactusLM.isDownloaded) {
+ return;
+ }
+
+ const setupIndex = async () => {
+ try {
+ // Initialize model and index
+ await cactusLM.init();
+ await cactusIndex.init();
+ } catch (e) {
+ console.error('Error during index setup:', e);
+ return;
+ }
+
+ try {
+ // Check if index is already populated
+ const existing = await cactusIndex.get({ ids: [0] });
+ if (existing.documents.length > 0) {
+ console.log('Index already populated, skipping setup.');
+ return;
+ }
+ } catch {}
+
+ const ids = [];
+ const documents = [];
+ const embeddings = [];
+ const metadatas = [];
+
+ for (const [i, doc] of SAMPLE_DOCUMENTS.entries()) {
+ ids.push(i);
+ documents.push(doc);
+ metadatas.push(JSON.stringify({ source: `Sample Document ${i}` }));
+ const embedding = await cactusLM.embed({ text: doc });
+ console.log(
+ `Generated embedding for document: "${doc}" with embedding length: ${embedding.embedding.length}`
+ );
+ embeddings.push(embedding.embedding);
+ }
+
+ try {
+ await cactusIndex.add({ ids, documents, embeddings, metadatas });
+ } catch (e) {
+ console.error('Error adding documents to index:', e);
+ }
+ };
+ setupIndex();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [cactusLM.isDownloaded]);
+
+ const indexAdd = async () => {
+ if (!newId || !newDoc) {
+ console.warn('Please provide both an ID and a document to add.');
+ return;
+ }
+
+ try {
+ const embedding = await cactusLM.embed({ text: newDoc });
+ await cactusIndex.add({
+ ids: [parseInt(newId, 10)],
+ documents: [newDoc],
+ embeddings: [embedding.embedding],
+ metadatas: [newMetadata],
+ });
+ console.log('Document added successfully');
+ setNewId('');
+ setNewDoc('');
+ setNewMetadata('');
+ } catch (e) {
+ console.error('Error adding document to index:', e);
+ }
+ };
+
+ const indexQuery = async () => {
+ if (!query) {
+ console.warn('Please provide a query string.');
+ return;
+ }
+
+ try {
+ const queryEmbedding = await cactusLM.embed({ text: query });
+ const queryResult = await cactusIndex.query({
+ embeddings: [queryEmbedding.embedding],
+ options: { topK: 3, scoreThreshold: 0.1 },
+ });
+
+ console.log('Query results:', queryResult);
+ setQueryResults(queryResult);
+
+ if (!queryResult.ids[0]) {
+ console.log('No results found for the query.');
+ return;
+ }
+
+ setGetResults(await cactusIndex.get({ ids: queryResult.ids[0] }));
+ } catch (e) {
+ console.error('Error querying index:', e);
+ return;
+ }
+ };
+
+ if (cactusLM.isDownloading) {
+ return (
+
+
+
+ Downloading model: {Math.round(cactusLM.downloadProgress * 100)}%
+
+
+ );
+ }
+
+ return (
+
+
+ Vector Index Demo
+
+ Sample documents have been indexed. Query them or add new documents.
+
+
+
+
+ Add Document
+
+
+
+
+ Add to Index
+
+
+
+
+ Query Index
+
+
+
+ {cactusLM.isGenerating ? 'Querying...' : 'Query'}
+
+
+
+
+ {getResults && queryResults && (
+
+ Results
+ {getResults.documents.map((doc, index) => (
+
+
+
+ {queryResults.scores[0]?.[index]?.toFixed(3) || 'N/A'}
+
+
+ {doc}
+ {getResults.metadatas[index] && (
+
+ {getResults.metadatas[index]}
+
+ )}
+
+ ))}
+
+ )}
+
+ {cactusLM.error && (
+
+ {cactusLM.error}
+
+ )}
+
+ );
+};
+
+export default IndexScreen;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#fff',
+ },
+ content: {
+ padding: 20,
+ },
+ centerContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ progressText: {
+ marginTop: 16,
+ fontSize: 16,
+ color: '#000',
+ },
+ infoBox: {
+ backgroundColor: '#f3f3f3',
+ padding: 12,
+ borderRadius: 8,
+ marginBottom: 16,
+ },
+ infoTitle: {
+ fontSize: 14,
+ fontWeight: '600',
+ marginBottom: 4,
+ color: '#000',
+ },
+ infoText: {
+ fontSize: 14,
+ color: '#666',
+ },
+ section: {
+ marginBottom: 24,
+ },
+ sectionTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 12,
+ color: '#000',
+ },
+ input: {
+ borderWidth: 1,
+ borderColor: '#ddd',
+ borderRadius: 8,
+ padding: 12,
+ fontSize: 16,
+ marginBottom: 12,
+ color: '#000',
+ },
+ multilineInput: {
+ textAlignVertical: 'top',
+ minHeight: 80,
+ },
+ buttonContainer: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 8,
+ marginTop: 16,
+ },
+ button: {
+ backgroundColor: '#000',
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 8,
+ alignItems: 'center',
+ },
+ buttonText: {
+ color: '#fff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ resultContainer: {
+ marginTop: 16,
+ marginBottom: 16,
+ },
+ resultLabel: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 8,
+ color: '#000',
+ },
+ resultBox: {
+ backgroundColor: '#f3f3f3',
+ padding: 12,
+ borderRadius: 8,
+ marginBottom: 12,
+ },
+ resultHeader: {
+ marginBottom: 8,
+ },
+ resultScore: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#000',
+ },
+ resultText: {
+ fontSize: 14,
+ color: '#000',
+ lineHeight: 20,
+ },
+ resultMetadata: {
+ fontSize: 12,
+ color: '#666',
+ marginTop: 8,
+ },
+ errorContainer: {
+ backgroundColor: '#000',
+ padding: 12,
+ borderRadius: 8,
+ marginTop: 16,
+ },
+ errorText: {
+ color: '#fff',
+ fontSize: 14,
+ },
+});
diff --git a/example/src/PerformanceScreen.tsx b/example/src/PerformanceScreen.tsx
deleted file mode 100644
index 4fc9384..0000000
--- a/example/src/PerformanceScreen.tsx
+++ /dev/null
@@ -1,313 +0,0 @@
-import { useState, useEffect } from 'react';
-import {
- View,
- Text,
- TextInput,
- TouchableOpacity,
- ScrollView,
- StyleSheet,
- ActivityIndicator,
-} from 'react-native';
-import {
- CactusLM,
- type Message,
- type CactusLMCompleteResult,
-} from 'cactus-react-native';
-
-const cactusLM = new CactusLM({ model: 'lfm2-350m' });
-
-const PerformanceScreen = () => {
- const [input, setInput] = useState('What is the capital of France?');
- const [result, setResult] = useState(null);
- const [isDownloading, setIsDownloading] = useState(false);
- const [downloadProgress, setDownloadProgress] = useState(0);
- const [isGenerating, setIsGenerating] = useState(false);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- const downloadModel = async () => {
- try {
- setIsDownloading(true);
- await cactusLM.download({
- onProgress: (progress) => {
- setDownloadProgress(progress);
- },
- });
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Download failed');
- } finally {
- setIsDownloading(false);
- }
- };
-
- downloadModel();
-
- // Cleanup on unmount
- return () => {
- cactusLM.destroy();
- };
- }, []);
-
- const handleInit = async () => {
- try {
- setError(null);
- await cactusLM.init();
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Init failed');
- }
- };
-
- const handleComplete = async () => {
- try {
- setError(null);
- setIsGenerating(true);
- const messages: Message[] = [
- { role: 'system', content: 'You are a helpful assistant.' },
- { role: 'user', content: input },
- ];
- const completionResult = await cactusLM.complete({ messages });
- setResult(completionResult);
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Complete failed');
- } finally {
- setIsGenerating(false);
- }
- };
-
- const handleStop = async () => {
- try {
- setError(null);
- await cactusLM.stop();
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Stop failed');
- }
- };
-
- const handleReset = async () => {
- try {
- setError(null);
- await cactusLM.reset();
- setResult(null);
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Reset failed');
- }
- };
-
- const handleDestroy = async () => {
- try {
- setError(null);
- await cactusLM.destroy();
- setResult(null);
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Destroy failed');
- }
- };
-
- if (isDownloading) {
- return (
-
-
-
- Downloading model: {Math.round(downloadProgress * 100)}%
-
-
- );
- }
-
- return (
-
-
-
-
-
- Init
-
-
-
-
- {isGenerating ? 'Completing...' : 'Complete'}
-
-
-
-
- Stop
-
-
-
- Reset
-
-
-
- Destroy
-
-
-
- {result && (
-
- CactusLMCompleteResult:
-
- success:
-
- {result.success.toString()}
-
-
-
- response:
-
- {result.response}
-
-
- functionCalls:
-
-
- {result.functionCalls
- ? JSON.stringify(result.functionCalls, null, 2)
- : 'undefined'}
-
-
-
- timeToFirstTokenMs:
-
-
- {result.timeToFirstTokenMs.toFixed(2)}
-
-
-
- totalTimeMs:
-
-
- {result.totalTimeMs.toFixed(2)}
-
-
-
- tokensPerSecond:
-
-
- {result.tokensPerSecond.toFixed(2)}
-
-
-
- prefillTokens:
-
- {result.prefillTokens}
-
-
- decodeTokens:
-
- {result.decodeTokens}
-
-
- totalTokens:
-
- {result.totalTokens}
-
-
- )}
-
- {error && (
-
- {error}
-
- )}
-
- );
-};
-
-export default PerformanceScreen;
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#fff',
- },
- content: {
- padding: 20,
- },
- centerContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- padding: 20,
- },
- progressText: {
- marginTop: 16,
- fontSize: 16,
- color: '#000',
- },
- input: {
- borderWidth: 1,
- borderColor: '#ddd',
- borderRadius: 8,
- padding: 12,
- fontSize: 16,
- textAlignVertical: 'top',
- marginBottom: 16,
- color: '#000',
- },
- buttonContainer: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 8,
- marginBottom: 16,
- },
- button: {
- backgroundColor: '#000',
- paddingVertical: 12,
- paddingHorizontal: 16,
- borderRadius: 8,
- alignItems: 'center',
- },
- buttonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
- resultContainer: {
- marginTop: 16,
- },
- resultLabel: {
- fontSize: 16,
- fontWeight: '600',
- marginBottom: 8,
- color: '#000',
- },
- resultBox: {
- backgroundColor: '#f3f3f3',
- padding: 12,
- borderRadius: 8,
- },
- resultFieldLabel: {
- fontSize: 12,
- fontWeight: '600',
- color: '#666',
- marginBottom: 4,
- },
- resultFieldValue: {
- fontSize: 14,
- color: '#000',
- lineHeight: 20,
- },
- marginTop: {
- marginTop: 12,
- },
- errorContainer: {
- backgroundColor: '#000',
- padding: 12,
- borderRadius: 8,
- marginTop: 16,
- },
- errorText: {
- color: '#fff',
- fontSize: 14,
- },
-});
diff --git a/example/src/STTScreen.tsx b/example/src/STTScreen.tsx
index 898c2e5..9a1bb4a 100644
--- a/example/src/STTScreen.tsx
+++ b/example/src/STTScreen.tsx
@@ -56,7 +56,7 @@ const STTScreen = () => {
return;
}
const transcribeResult = await cactusSTT.transcribe({
- audioFilePath: audioFile,
+ audio: audioFile,
});
setResult(transcribeResult);
};
diff --git a/example/src/ToolCallingScreen.tsx b/example/src/ToolCallingScreen.tsx
index d74707b..60d5282 100644
--- a/example/src/ToolCallingScreen.tsx
+++ b/example/src/ToolCallingScreen.tsx
@@ -53,7 +53,11 @@ const ToolCallingScreen = () => {
{ role: 'user', content: input },
];
- const completionResult = await cactusLM.complete({ messages, tools });
+ const completionResult = await cactusLM.complete({
+ messages,
+ tools,
+ options: { forceTools: true },
+ });
setResult(completionResult);
};
diff --git a/ios/HybridCactusFileSystem.swift b/ios/HybridCactusFileSystem.swift
index 0685700..dd342cc 100644
--- a/ios/HybridCactusFileSystem.swift
+++ b/ios/HybridCactusFileSystem.swift
@@ -133,7 +133,11 @@ class HybridCactusFileSystem: HybridCactusFileSystemSpec {
try FileManager.default.removeItem(at: modelURL)
}
}
-
+
+ func getIndexPath(name: String) throws -> Promise {
+ return Promise.async { try self.indexURL(name: name).path }
+ }
+
private func cactusURL() throws -> URL {
let documentsURL = try FileManager.default.url(
for: .documentDirectory,
@@ -156,7 +160,24 @@ class HybridCactusFileSystem: HybridCactusFileSystemSpec {
private func modelURL(model: String) throws -> URL {
let cactusURL = try self.cactusURL()
- return cactusURL.appendingPathComponent("models/\(model)")
+ let modelsURL = cactusURL.appendingPathComponent("models", isDirectory: true)
+
+ if !FileManager.default.fileExists(atPath: modelsURL.path) {
+ try FileManager.default.createDirectory(at: modelsURL, withIntermediateDirectories: true)
+ }
+
+ return modelsURL.appendingPathComponent(model)
+ }
+
+ private func indexURL(name: String) throws -> URL {
+ let cactusURL = try self.cactusURL()
+ let finalURL = cactusURL.appendingPathComponent("indexes/\(name)", isDirectory: true)
+
+ if !FileManager.default.fileExists(atPath: finalURL.path) {
+ try FileManager.default.createDirectory(at: finalURL, withIntermediateDirectories: true)
+ }
+
+ return finalURL
}
}
diff --git a/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus.h b/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus.h
index a43d6ef..7278013 100644
--- a/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus.h
+++ b/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus.h
@@ -7,5 +7,7 @@
#include "engine/engine.h"
#include "models/model.h"
#include "ffi/cactus_ffi.h"
+#include "ffi/cactus_telemetry.h"
+#include "npu/npu.h"
#endif // CACTUS_H
\ No newline at end of file
diff --git a/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus_ffi.h b/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus_ffi.h
index 6bb3f27..e00b391 100644
--- a/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus_ffi.h
+++ b/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus_ffi.h
@@ -3,6 +3,7 @@
#include
#include
+#include
#if __GNUC__ >= 4
#define CACTUS_FFI_EXPORT __attribute__ ((visibility ("default")))
@@ -33,6 +34,26 @@ CACTUS_FFI_EXPORT int cactus_complete(
void* user_data
);
+CACTUS_FFI_EXPORT int cactus_tokenize(
+ cactus_model_t model,
+ const char* text,
+ uint32_t* token_buffer,
+ size_t token_buffer_len,
+ size_t* out_token_len
+);
+
+CACTUS_FFI_EXPORT int cactus_score_window(
+ cactus_model_t model,
+ const uint32_t* tokens,
+ size_t token_len,
+ size_t start,
+ size_t end,
+ size_t context,
+ char* response_buffer,
+ size_t buffer_size
+);
+
+
CACTUS_FFI_EXPORT int cactus_transcribe(
cactus_model_t model,
const char* audio_file_path,
@@ -41,7 +62,9 @@ CACTUS_FFI_EXPORT int cactus_transcribe(
size_t buffer_size,
const char* options_json,
cactus_token_callback callback,
- void* user_data
+ void* user_data,
+ const uint8_t* pcm_buffer,
+ size_t pcm_buffer_size
);
@@ -50,7 +73,8 @@ CACTUS_FFI_EXPORT int cactus_embed(
const char* text,
float* embeddings_buffer,
size_t buffer_size,
- size_t* embedding_dim
+ size_t* embedding_dim,
+ bool normalize
);
CACTUS_FFI_EXPORT int cactus_image_embed(
@@ -75,6 +99,63 @@ CACTUS_FFI_EXPORT void cactus_stop(cactus_model_t model);
CACTUS_FFI_EXPORT void cactus_destroy(cactus_model_t model);
+CACTUS_FFI_EXPORT const char* cactus_get_last_error(void);
+
+CACTUS_FFI_EXPORT void cactus_set_telemetry_token(const char* token);
+
+CACTUS_FFI_EXPORT void cactus_set_pro_key(const char* pro_key);
+
+typedef void* cactus_index_t;
+
+CACTUS_FFI_EXPORT cactus_index_t cactus_index_init(
+ const char* index_dir,
+ size_t embedding_dim
+);
+
+CACTUS_FFI_EXPORT int cactus_index_add(
+ cactus_index_t index,
+ const int* ids,
+ const char** documents,
+ const char** metadatas,
+ const float** embeddings,
+ size_t count,
+ size_t embedding_dim
+);
+
+CACTUS_FFI_EXPORT int cactus_index_delete(
+ cactus_index_t index,
+ const int* ids,
+ size_t ids_count
+);
+
+CACTUS_FFI_EXPORT int cactus_index_get(
+ cactus_index_t index,
+ const int* ids,
+ size_t ids_count,
+ char** document_buffers,
+ size_t* document_buffer_sizes,
+ char** metadata_buffers,
+ size_t* metadata_buffer_sizes,
+ float** embedding_buffers,
+ size_t* embedding_buffer_sizes
+);
+
+CACTUS_FFI_EXPORT int cactus_index_query(
+ cactus_index_t index,
+ const float** embeddings,
+ size_t embeddings_count,
+ size_t embedding_dim,
+ const char* options_json,
+ int** id_buffers,
+ size_t* id_buffer_sizes,
+ float** score_buffers,
+ size_t* score_buffer_sizes
+);
+
+CACTUS_FFI_EXPORT int cactus_index_compact(cactus_index_t index);
+
+CACTUS_FFI_EXPORT void cactus_index_destroy(cactus_index_t index);
+
#ifdef __cplusplus
}
#endif
diff --git a/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus_telemetry.h b/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus_telemetry.h
new file mode 100644
index 0000000..a48b6fc
--- /dev/null
+++ b/ios/cactus.xcframework/ios-arm64-simulator/cactus.framework/Headers/cactus_telemetry.h
@@ -0,0 +1,656 @@
+#ifndef CACTUS_TELEMETRY_H
+#define CACTUS_TELEMETRY_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include