react-native-nobodywho 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Nobodywho.podspec +2 -2
- package/README.md +19 -1
- package/generated/cpp/nobodywho.cpp +1 -1
- package/generated/ts/nobodywho-ffi.ts +1 -1
- package/generated/ts/nobodywho.ts +42 -70
- package/ios/Nobodywho.h +1 -1
- package/package.json +1 -1
- package/src/chat.ts +28 -20
- package/src/cross_encoder.ts +67 -0
- package/src/encoder.ts +51 -0
- package/src/message.ts +75 -0
- package/src/model.ts +63 -0
- package/src/sampler_presets.ts +9 -9
- package/src/wrapper.ts +11 -19
package/Nobodywho.podspec
CHANGED
|
@@ -43,10 +43,10 @@ Pod::Spec.new do |s|
|
|
|
43
43
|
s.summary = package["description"]
|
|
44
44
|
s.homepage = package["homepage"]
|
|
45
45
|
s.license = package["license"]
|
|
46
|
-
s.authors = package["author"]
|
|
46
|
+
s.authors = package["author"] || { "NobodyWho" => "info@nobodywho.ooo" }
|
|
47
47
|
|
|
48
48
|
s.platforms = { :ios => min_ios_version_supported }
|
|
49
|
-
s.source = { :git => "https://github.com/nobodywho-ooo/nobodywho", :tag => "#{s.version}" }
|
|
49
|
+
s.source = { :git => "https://github.com/nobodywho-ooo/nobodywho.git", :tag => "#{s.version}" }
|
|
50
50
|
|
|
51
51
|
s.source_files = "ios/**/*.{h,m,mm,swift}", "ios/generated/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "generated/cpp/**/*.{hpp,cpp,c,h}"
|
|
52
52
|
s.vendored_frameworks = framework_name
|
package/README.md
CHANGED
|
@@ -30,13 +30,31 @@ This library uses the **GGUF format** — a binary format optimized for fast loa
|
|
|
30
30
|
- iOS: iPhone 11 or newer with at least 4 GB of RAM.
|
|
31
31
|
- Android: Snapdragon 855 / Adreno 640 / 6 GB RAM or better.
|
|
32
32
|
|
|
33
|
+
## Model Loading
|
|
34
|
+
|
|
35
|
+
Models can be loaded from a local file path or downloaded automatically from HuggingFace:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { Model } from "react-native-nobodywho";
|
|
39
|
+
|
|
40
|
+
// Download from HuggingFace (cached automatically)
|
|
41
|
+
const model = await Model.load({
|
|
42
|
+
modelPath: "hf://NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Or load from a local file
|
|
46
|
+
const model = await Model.load({ modelPath: "/path/to/model.gguf" });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Downloaded models are cached on disk and reused on subsequent loads.
|
|
50
|
+
|
|
33
51
|
## Chat
|
|
34
52
|
|
|
35
53
|
```typescript
|
|
36
54
|
import { Chat } from "react-native-nobodywho";
|
|
37
55
|
|
|
38
56
|
const chat = await Chat.fromPath({
|
|
39
|
-
modelPath: "/
|
|
57
|
+
modelPath: "hf://NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf",
|
|
40
58
|
systemPrompt: "You are a helpful assistant.",
|
|
41
59
|
});
|
|
42
60
|
|
|
@@ -249,7 +249,7 @@ void uniffi_nobodywho_uniffi_fn_init_callback_vtable_rusttoolcallback(
|
|
|
249
249
|
float uniffi_nobodywho_uniffi_fn_func_cosine_similarity(
|
|
250
250
|
RustBuffer a, RustBuffer b, RustCallStatus *uniffi_out_err);
|
|
251
251
|
/*handle*/ uint64_t uniffi_nobodywho_uniffi_fn_func_load_model(
|
|
252
|
-
RustBuffer model_path, int8_t use_gpu, RustBuffer
|
|
252
|
+
RustBuffer model_path, int8_t use_gpu, RustBuffer projection_model_path);
|
|
253
253
|
/*handle*/ uint64_t uniffi_nobodywho_uniffi_fn_func_sampler_preset_default(
|
|
254
254
|
RustCallStatus *uniffi_out_err);
|
|
255
255
|
/*handle*/ uint64_t uniffi_nobodywho_uniffi_fn_func_sampler_preset_dry(
|
|
@@ -76,7 +76,7 @@ interface NativeModuleInterface {
|
|
|
76
76
|
ubrn_uniffi_nobodywho_uniffi_fn_method_samplerconfig_to_json(ptr: bigint, uniffi_out_err: UniffiRustCallStatus): Uint8Array;
|
|
77
77
|
ubrn_uniffi_nobodywho_uniffi_fn_init_callback_vtable_rusttoolcallback(vtable: UniffiVTableCallbackInterfaceRustToolCallback): void;
|
|
78
78
|
ubrn_uniffi_nobodywho_uniffi_fn_func_cosine_similarity(a: Uint8Array, b: Uint8Array, uniffi_out_err: UniffiRustCallStatus): number;
|
|
79
|
-
ubrn_uniffi_nobodywho_uniffi_fn_func_load_model(modelPath: Uint8Array, useGpu: number,
|
|
79
|
+
ubrn_uniffi_nobodywho_uniffi_fn_func_load_model(modelPath: Uint8Array, useGpu: number, projectionModelPath: Uint8Array): bigint;
|
|
80
80
|
ubrn_uniffi_nobodywho_uniffi_fn_func_sampler_preset_default(uniffi_out_err: UniffiRustCallStatus): bigint;
|
|
81
81
|
ubrn_uniffi_nobodywho_uniffi_fn_func_sampler_preset_dry(uniffi_out_err: UniffiRustCallStatus): bigint;
|
|
82
82
|
ubrn_uniffi_nobodywho_uniffi_fn_func_sampler_preset_grammar(grammar: Uint8Array, uniffi_out_err: UniffiRustCallStatus): bigint;
|
|
@@ -95,18 +95,22 @@ export function cosineSimilarity(a: Array</*f32*/number>, b: Array</*f32*/number
|
|
|
95
95
|
));
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
* Load a GGUF model from
|
|
98
|
+
* Load a GGUF model from a local path or remote URL.
|
|
99
|
+
*
|
|
100
|
+
* Accepts local filesystem paths, `hf://owner/repo/file.gguf` for HuggingFace downloads,
|
|
101
|
+
* or `https://` URLs. Downloaded models are cached automatically.
|
|
102
|
+
*
|
|
99
103
|
* This is a free function instead of an async constructor because
|
|
100
104
|
* uniffi-bindgen-react-native generates invalid JS (`async static` instead
|
|
101
105
|
* of `static async`) for async constructors.
|
|
102
106
|
*/
|
|
103
|
-
export async function loadModel(modelPath: string, useGpu: boolean,
|
|
107
|
+
export async function loadModel(modelPath: string, useGpu: boolean, projectionModelPath: string | undefined, asyncOpts_?: { signal: AbortSignal }): Promise<ModelInterface> /*throws*/ {
|
|
104
108
|
const __stack = uniffiIsDebug ? new Error().stack : undefined;
|
|
105
109
|
try {
|
|
106
110
|
return await uniffiRustCallAsync(
|
|
107
111
|
/*rustCaller:*/ uniffiCaller,
|
|
108
112
|
/*rustFutureFunc:*/ () => {
|
|
109
|
-
return nativeModule().ubrn_uniffi_nobodywho_uniffi_fn_func_load_model(FfiConverterString.lower(modelPath),FfiConverterBool.lower(useGpu),FfiConverterOptionalString.lower(
|
|
113
|
+
return nativeModule().ubrn_uniffi_nobodywho_uniffi_fn_func_load_model(FfiConverterString.lower(modelPath),FfiConverterBool.lower(useGpu),FfiConverterOptionalString.lower(projectionModelPath)
|
|
110
114
|
);
|
|
111
115
|
},
|
|
112
116
|
/*pollFunc:*/ nativeModule().ubrn_ffi_nobodywho_uniffi_rust_future_poll_u64,
|
|
@@ -693,108 +697,76 @@ const FfiConverterTypeMessage = (() => {
|
|
|
693
697
|
|
|
694
698
|
|
|
695
699
|
|
|
696
|
-
// Error type: NobodyWhoError
|
|
697
700
|
|
|
698
|
-
//
|
|
701
|
+
// Flat error type: NobodyWhoError
|
|
699
702
|
export enum NobodyWhoError_Tags {
|
|
700
703
|
Error = "Error"
|
|
701
704
|
}
|
|
702
705
|
export const NobodyWhoError = (() => {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
type Error__interface = {
|
|
706
|
-
tag: NobodyWhoError_Tags.Error;
|
|
707
|
-
inner: Readonly<{message: string}>
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
class Error_ extends UniffiError implements Error__interface {
|
|
706
|
+
class Error extends UniffiError {
|
|
712
707
|
/**
|
|
713
708
|
* @private
|
|
714
|
-
* This field is private and should not be used
|
|
709
|
+
* This field is private and should not be used.
|
|
715
710
|
*/
|
|
716
|
-
readonly [uniffiTypeNameSymbol] = "NobodyWhoError";
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
static new(inner: { message: string }): Error_ {
|
|
725
|
-
return new Error_(inner);
|
|
726
|
-
}
|
|
711
|
+
readonly [uniffiTypeNameSymbol]: string = "NobodyWhoError";
|
|
712
|
+
/**
|
|
713
|
+
* @private
|
|
714
|
+
* This field is private and should not be used.
|
|
715
|
+
*/
|
|
716
|
+
readonly [variantOrdinalSymbol] = 1;
|
|
727
717
|
|
|
728
|
-
|
|
729
|
-
return obj.tag === NobodyWhoError_Tags.Error;
|
|
730
|
-
}
|
|
731
|
-
|
|
718
|
+
readonly tag = NobodyWhoError_Tags.Error;
|
|
732
719
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
return Error_.instanceOf(obj);
|
|
720
|
+
constructor(message: string) {
|
|
721
|
+
super("NobodyWhoError", "Error", message);
|
|
736
722
|
}
|
|
737
723
|
|
|
738
|
-
static
|
|
739
|
-
return
|
|
724
|
+
static instanceOf(e: any): e is Error {
|
|
725
|
+
return (
|
|
726
|
+
instanceOf(e) && (e as any)[variantOrdinalSymbol] === 1
|
|
727
|
+
);
|
|
740
728
|
}
|
|
741
|
-
|
|
742
729
|
}
|
|
743
730
|
|
|
744
|
-
function
|
|
745
|
-
|
|
731
|
+
// Utility function which does not rely on instanceof.
|
|
732
|
+
function instanceOf(e: any): e is NobodyWhoError {
|
|
733
|
+
return (e as any)[uniffiTypeNameSymbol] === "NobodyWhoError";
|
|
746
734
|
}
|
|
747
|
-
|
|
748
|
-
|
|
735
|
+
return {
|
|
736
|
+
Error,
|
|
749
737
|
instanceOf,
|
|
750
|
-
|
|
751
|
-
});
|
|
752
|
-
|
|
738
|
+
};
|
|
753
739
|
})();
|
|
754
740
|
|
|
741
|
+
// Union type for NobodyWhoError error type.
|
|
755
742
|
|
|
756
743
|
|
|
757
744
|
export type NobodyWhoError = InstanceType<
|
|
758
745
|
typeof NobodyWhoError[keyof Omit<typeof NobodyWhoError, 'instanceOf'>]
|
|
759
746
|
>;
|
|
760
747
|
|
|
761
|
-
// FfiConverter for enum NobodyWhoError
|
|
762
748
|
const FfiConverterTypeNobodyWhoError = (() => {
|
|
763
|
-
const
|
|
749
|
+
const intConverter = FfiConverterInt32;
|
|
764
750
|
type TypeName = NobodyWhoError;
|
|
765
|
-
class
|
|
751
|
+
class FfiConverter extends AbstractFfiConverterByteArray<TypeName> {
|
|
766
752
|
read(from: RustBuffer): TypeName {
|
|
767
|
-
switch (
|
|
768
|
-
case 1: return new NobodyWhoError.Error(
|
|
753
|
+
switch (intConverter.read(from)) {
|
|
754
|
+
case 1: return new NobodyWhoError.Error(FfiConverterString.read(from)
|
|
755
|
+
);
|
|
756
|
+
|
|
769
757
|
default: throw new UniffiInternalError.UnexpectedEnumCase();
|
|
770
758
|
}
|
|
771
759
|
}
|
|
772
760
|
write(value: TypeName, into: RustBuffer): void {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const inner = value.inner;
|
|
777
|
-
FfiConverterString.write(inner.message, into);
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
default:
|
|
781
|
-
// Throwing from here means that NobodyWhoError_Tags hasn't matched an ordinal.
|
|
782
|
-
throw new UniffiInternalError.UnexpectedEnumCase();
|
|
783
|
-
}
|
|
761
|
+
const obj = value as any;
|
|
762
|
+
const index = obj[variantOrdinalSymbol] as number;
|
|
763
|
+
intConverter.write(index, into);
|
|
784
764
|
}
|
|
785
765
|
allocationSize(value: TypeName): number {
|
|
786
|
-
|
|
787
|
-
case NobodyWhoError_Tags.Error: {
|
|
788
|
-
const inner = value.inner;
|
|
789
|
-
let size = ordinalConverter.allocationSize(1);
|
|
790
|
-
size += FfiConverterString.allocationSize(inner.message);
|
|
791
|
-
return size;
|
|
792
|
-
}
|
|
793
|
-
default: throw new UniffiInternalError.UnexpectedEnumCase();
|
|
794
|
-
}
|
|
766
|
+
return intConverter.allocationSize(0);
|
|
795
767
|
}
|
|
796
768
|
}
|
|
797
|
-
return new
|
|
769
|
+
return new FfiConverter();
|
|
798
770
|
})();
|
|
799
771
|
|
|
800
772
|
|
|
@@ -2872,7 +2844,7 @@ function uniffiEnsureInitialized() {
|
|
|
2872
2844
|
if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_cosine_similarity() !== 63439) {
|
|
2873
2845
|
throw new UniffiInternalError.ApiChecksumMismatch("uniffi_nobodywho_uniffi_checksum_func_cosine_similarity");
|
|
2874
2846
|
}
|
|
2875
|
-
if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_load_model() !==
|
|
2847
|
+
if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_load_model() !== 57455) {
|
|
2876
2848
|
throw new UniffiInternalError.ApiChecksumMismatch("uniffi_nobodywho_uniffi_checksum_func_load_model");
|
|
2877
2849
|
}
|
|
2878
2850
|
if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_sampler_preset_default() !== 10834) {
|
package/ios/Nobodywho.h
CHANGED
package/package.json
CHANGED
package/src/chat.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RustChat,
|
|
3
|
-
|
|
4
|
-
type ModelInterface,
|
|
5
|
-
type SamplerConfigInterface,
|
|
6
|
-
type Message,
|
|
3
|
+
SamplerConfig,
|
|
7
4
|
} from "../generated/ts/nobodywho";
|
|
5
|
+
import { Model } from "./model";
|
|
6
|
+
import { type ChatMessage, fromInternal, toInternal } from "./message";
|
|
8
7
|
import { TokenStream } from "./streaming";
|
|
9
8
|
import type { Prompt } from "./prompt";
|
|
10
9
|
import type { Tool } from "./tool";
|
|
@@ -17,7 +16,7 @@ import type { Tool } from "./tool";
|
|
|
17
16
|
*
|
|
18
17
|
* @example
|
|
19
18
|
* ```typescript
|
|
20
|
-
* const model = await
|
|
19
|
+
* const model = await Model.load({ modelPath: "model.gguf" });
|
|
21
20
|
* const chat = new Chat({
|
|
22
21
|
* model,
|
|
23
22
|
* systemPrompt: "You are a helpful assistant.",
|
|
@@ -32,15 +31,15 @@ export class Chat {
|
|
|
32
31
|
private readonly _inner: RustChat;
|
|
33
32
|
|
|
34
33
|
constructor(opts: {
|
|
35
|
-
model:
|
|
34
|
+
model: Model;
|
|
36
35
|
systemPrompt?: string;
|
|
37
36
|
contextSize?: number;
|
|
38
37
|
templateVariables?: Map<string, boolean>;
|
|
39
38
|
tools?: Tool[];
|
|
40
|
-
sampler?:
|
|
39
|
+
sampler?: SamplerConfig;
|
|
41
40
|
}) {
|
|
42
41
|
this._inner = new RustChat(
|
|
43
|
-
opts.model,
|
|
42
|
+
opts.model._inner,
|
|
44
43
|
opts.systemPrompt ?? undefined,
|
|
45
44
|
opts.contextSize ?? 4096,
|
|
46
45
|
opts.templateVariables ?? undefined,
|
|
@@ -64,18 +63,18 @@ export class Chat {
|
|
|
64
63
|
static async fromPath(opts: {
|
|
65
64
|
modelPath: string;
|
|
66
65
|
useGpu?: boolean;
|
|
67
|
-
|
|
66
|
+
projectionModelPath?: string;
|
|
68
67
|
systemPrompt?: string;
|
|
69
68
|
contextSize?: number;
|
|
70
69
|
templateVariables?: Map<string, boolean>;
|
|
71
70
|
tools?: Tool[];
|
|
72
|
-
sampler?:
|
|
71
|
+
sampler?: SamplerConfig;
|
|
73
72
|
}): Promise<Chat> {
|
|
74
|
-
const model = await
|
|
75
|
-
opts.modelPath,
|
|
76
|
-
opts.useGpu
|
|
77
|
-
opts.
|
|
78
|
-
);
|
|
73
|
+
const model = await Model.load({
|
|
74
|
+
modelPath: opts.modelPath,
|
|
75
|
+
useGpu: opts.useGpu,
|
|
76
|
+
projectionModelPath: opts.projectionModelPath,
|
|
77
|
+
});
|
|
79
78
|
return new Chat({ model, ...opts });
|
|
80
79
|
}
|
|
81
80
|
|
|
@@ -109,13 +108,14 @@ export class Chat {
|
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
/** Get the current chat history as a list of messages. */
|
|
112
|
-
async getChatHistory(): Promise<
|
|
113
|
-
|
|
111
|
+
async getChatHistory(): Promise<ChatMessage[]> {
|
|
112
|
+
const internal = await this._inner.getChatHistory();
|
|
113
|
+
return internal.map(fromInternal);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/** Set the chat history from a list of messages. */
|
|
117
|
-
async setChatHistory(messages:
|
|
118
|
-
return this._inner.setChatHistory(messages);
|
|
117
|
+
async setChatHistory(messages: ChatMessage[]): Promise<void> {
|
|
118
|
+
return this._inner.setChatHistory(messages.map(toInternal));
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/** Get the current system prompt. */
|
|
@@ -144,7 +144,7 @@ export class Chat {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/** Set the sampler configuration. */
|
|
147
|
-
async setSamplerConfig(sampler:
|
|
147
|
+
async setSamplerConfig(sampler: SamplerConfig): Promise<void> {
|
|
148
148
|
return this._inner.setSamplerConfig(sampler);
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -152,4 +152,12 @@ export class Chat {
|
|
|
152
152
|
async getSamplerConfigJson(): Promise<string> {
|
|
153
153
|
return this._inner.getSamplerConfigJson();
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Immediately free the underlying Rust resources (model context, KV cache, etc.).
|
|
158
|
+
* After calling this, the Chat instance is no longer usable.
|
|
159
|
+
*/
|
|
160
|
+
destroy(): void {
|
|
161
|
+
this._inner.uniffiDestroy();
|
|
162
|
+
}
|
|
155
163
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { CrossEncoder as RustCrossEncoder } from "../generated/ts/nobodywho";
|
|
2
|
+
import { Model } from "./model";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A cross-encoder for ranking documents by relevance to a query.
|
|
6
|
+
*
|
|
7
|
+
* Wraps the internal CrossEncoder with an ergonomic API including
|
|
8
|
+
* a `fromPath` factory for one-step model loading.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const crossEncoder = await CrossEncoder.fromPath({
|
|
13
|
+
* modelPath: "crossencoder.gguf",
|
|
14
|
+
* });
|
|
15
|
+
* const scores = await crossEncoder.rank("query", ["doc1", "doc2"]);
|
|
16
|
+
* const sorted = await crossEncoder.rankAndSort("query", ["doc1", "doc2"]);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class CrossEncoder {
|
|
20
|
+
/** @internal */
|
|
21
|
+
private readonly _inner: RustCrossEncoder;
|
|
22
|
+
|
|
23
|
+
constructor(opts: { model: Model; contextSize?: number }) {
|
|
24
|
+
this._inner = new RustCrossEncoder(
|
|
25
|
+
opts.model._inner,
|
|
26
|
+
opts.contextSize ?? undefined,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a cross-encoder directly from a model path.
|
|
32
|
+
* Loads the model and creates the cross-encoder in one step.
|
|
33
|
+
*/
|
|
34
|
+
static async fromPath(opts: {
|
|
35
|
+
modelPath: string;
|
|
36
|
+
useGpu?: boolean;
|
|
37
|
+
contextSize?: number;
|
|
38
|
+
}): Promise<CrossEncoder> {
|
|
39
|
+
const model = await Model.load({ modelPath: opts.modelPath, useGpu: opts.useGpu });
|
|
40
|
+
return new CrossEncoder({ model, ...opts });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Rank documents by relevance to a query. Returns similarity scores. */
|
|
44
|
+
async rank(query: string, documents: string[]): Promise<number[]> {
|
|
45
|
+
return this._inner.rank(query, documents);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Rank documents and return them sorted by descending relevance.
|
|
50
|
+
* Returns an array of [document, score] pairs.
|
|
51
|
+
*/
|
|
52
|
+
async rankAndSort(
|
|
53
|
+
query: string,
|
|
54
|
+
documents: string[],
|
|
55
|
+
): Promise<[string, number][]> {
|
|
56
|
+
const json = await this._inner.rankAndSortJson(query, documents);
|
|
57
|
+
return JSON.parse(json);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Immediately free the underlying Rust resources.
|
|
62
|
+
* After calling this, the CrossEncoder instance is no longer usable.
|
|
63
|
+
*/
|
|
64
|
+
destroy(): void {
|
|
65
|
+
this._inner.uniffiDestroy();
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/encoder.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Encoder as RustEncoder } from "../generated/ts/nobodywho";
|
|
2
|
+
import { Model } from "./model";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An embedding encoder for generating text embeddings.
|
|
6
|
+
*
|
|
7
|
+
* Wraps the internal Encoder with an ergonomic API including
|
|
8
|
+
* a `fromPath` factory for one-step model loading.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const encoder = await Encoder.fromPath({
|
|
13
|
+
* modelPath: "embeddings.gguf",
|
|
14
|
+
* });
|
|
15
|
+
* const embedding = await encoder.encode("Hello world");
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export class Encoder {
|
|
19
|
+
/** @internal */
|
|
20
|
+
private readonly _inner: RustEncoder;
|
|
21
|
+
|
|
22
|
+
constructor(opts: { model: Model; contextSize?: number }) {
|
|
23
|
+
this._inner = new RustEncoder(opts.model._inner, opts.contextSize ?? undefined);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create an encoder directly from a model path.
|
|
28
|
+
* Loads the model and creates the encoder in one step.
|
|
29
|
+
*/
|
|
30
|
+
static async fromPath(opts: {
|
|
31
|
+
modelPath: string;
|
|
32
|
+
useGpu?: boolean;
|
|
33
|
+
contextSize?: number;
|
|
34
|
+
}): Promise<Encoder> {
|
|
35
|
+
const model = await Model.load({ modelPath: opts.modelPath, useGpu: opts.useGpu });
|
|
36
|
+
return new Encoder({ model, ...opts });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Encode text into an embedding vector. */
|
|
40
|
+
async encode(text: string): Promise<number[]> {
|
|
41
|
+
return this._inner.encode(text);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Immediately free the underlying Rust resources.
|
|
46
|
+
* After calling this, the Encoder instance is no longer usable.
|
|
47
|
+
*/
|
|
48
|
+
destroy(): void {
|
|
49
|
+
this._inner.uniffiDestroy();
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/message.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Message as InternalMessage,
|
|
3
|
+
Message_Tags,
|
|
4
|
+
Role,
|
|
5
|
+
type Asset,
|
|
6
|
+
type ToolCall,
|
|
7
|
+
} from "../generated/ts/nobodywho";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A chat message. The `role` field combined with the presence of
|
|
11
|
+
* `toolCalls` or `name` determines the message type:
|
|
12
|
+
*
|
|
13
|
+
* - **User/Assistant/System message:** `{ role, content, assets? }`
|
|
14
|
+
* - **Tool call request:** `{ role: Role.Assistant, content, toolCalls }`
|
|
15
|
+
* - **Tool response:** `{ role: Role.Tool, name, content }`
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const history = await chat.getChatHistory();
|
|
20
|
+
* for (const msg of history) {
|
|
21
|
+
* if ("toolCalls" in msg) {
|
|
22
|
+
* console.log("Tool calls:", msg.toolCalls);
|
|
23
|
+
* } else if ("name" in msg) {
|
|
24
|
+
* console.log("Tool response:", msg.name, msg.content);
|
|
25
|
+
* } else {
|
|
26
|
+
* console.log(msg.role, msg.content);
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export type ChatMessage =
|
|
32
|
+
| { role: Role.User | Role.Assistant | Role.System; content: string; assets?: Asset[] }
|
|
33
|
+
| { role: Role.Assistant; content: string; toolCalls: ToolCall[] }
|
|
34
|
+
| { role: Role.Tool; name: string; content: string };
|
|
35
|
+
|
|
36
|
+
/** @internal Convert internal Message to ChatMessage */
|
|
37
|
+
export function fromInternal(msg: InternalMessage): ChatMessage {
|
|
38
|
+
if (msg.tag === Message_Tags.Message) {
|
|
39
|
+
const { role, content, assets } = msg.inner;
|
|
40
|
+
return {
|
|
41
|
+
role: role as Role.User | Role.Assistant | Role.System,
|
|
42
|
+
content,
|
|
43
|
+
...(assets.length > 0 ? { assets } : {}),
|
|
44
|
+
};
|
|
45
|
+
} else if (msg.tag === Message_Tags.ToolCalls) {
|
|
46
|
+
const { role, content, toolCalls } = msg.inner;
|
|
47
|
+
return { role: role as Role.Assistant, content, toolCalls };
|
|
48
|
+
} else {
|
|
49
|
+
const { role, name, content } = msg.inner;
|
|
50
|
+
return { role: role as Role.Tool, name, content };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @internal Convert ChatMessage to internal Message */
|
|
55
|
+
export function toInternal(msg: ChatMessage): InternalMessage {
|
|
56
|
+
if ("toolCalls" in msg) {
|
|
57
|
+
return new InternalMessage.ToolCalls({
|
|
58
|
+
role: msg.role,
|
|
59
|
+
content: msg.content,
|
|
60
|
+
toolCalls: msg.toolCalls,
|
|
61
|
+
});
|
|
62
|
+
} else if ("name" in msg) {
|
|
63
|
+
return new InternalMessage.ToolResp({
|
|
64
|
+
role: msg.role,
|
|
65
|
+
name: msg.name,
|
|
66
|
+
content: msg.content,
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
return new InternalMessage.Message({
|
|
70
|
+
role: msg.role,
|
|
71
|
+
content: msg.content,
|
|
72
|
+
assets: msg.assets ?? [],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadModel,
|
|
3
|
+
type ModelInterface,
|
|
4
|
+
} from "../generated/ts/nobodywho";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A loaded LLM model that can be shared across Chat, Encoder, and CrossEncoder instances.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // Load from a local file
|
|
12
|
+
* const model = await Model.load({ modelPath: "model.gguf" });
|
|
13
|
+
* const chat = new Chat({ model, systemPrompt: "You are helpful." });
|
|
14
|
+
*
|
|
15
|
+
* // Download from HuggingFace (cached automatically)
|
|
16
|
+
* const model = await Model.load({
|
|
17
|
+
* modelPath: "hf://NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf",
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // With vision support
|
|
21
|
+
* const visionModel = await Model.load({
|
|
22
|
+
* modelPath: "model.gguf",
|
|
23
|
+
* projectionModelPath: "mmproj.gguf",
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class Model {
|
|
28
|
+
/** @internal */
|
|
29
|
+
readonly _inner: ModelInterface;
|
|
30
|
+
|
|
31
|
+
/** @internal */
|
|
32
|
+
private constructor(inner: ModelInterface) {
|
|
33
|
+
this._inner = inner;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load a GGUF model from a local path or remote URL.
|
|
38
|
+
*
|
|
39
|
+
* @param opts.modelPath - Path to the GGUF model file, or a `hf://owner/repo/file.gguf` / `https://` URL to download and cache automatically
|
|
40
|
+
* @param opts.useGpu - Use GPU acceleration if available (default: true)
|
|
41
|
+
* @param opts.projectionModelPath - Path to a multimodal projector file for vision models
|
|
42
|
+
*/
|
|
43
|
+
static async load(opts: {
|
|
44
|
+
modelPath: string;
|
|
45
|
+
useGpu?: boolean;
|
|
46
|
+
projectionModelPath?: string;
|
|
47
|
+
}): Promise<Model> {
|
|
48
|
+
const inner = await loadModel(
|
|
49
|
+
opts.modelPath,
|
|
50
|
+
opts.useGpu ?? true,
|
|
51
|
+
opts.projectionModelPath,
|
|
52
|
+
);
|
|
53
|
+
return new Model(inner);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Immediately free the underlying Rust resources (model weights, GPU memory).
|
|
58
|
+
* After calling this, the Model instance is no longer usable.
|
|
59
|
+
*/
|
|
60
|
+
destroy(): void {
|
|
61
|
+
(this._inner as any).uniffiDestroy();
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/sampler_presets.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SamplerConfigInterface } from "../generated/ts/nobodywho";
|
|
2
1
|
import {
|
|
2
|
+
type SamplerConfig,
|
|
3
3
|
samplerPresetDefault,
|
|
4
4
|
samplerPresetDry,
|
|
5
5
|
samplerPresetGrammar,
|
|
@@ -23,42 +23,42 @@ export class SamplerPresets {
|
|
|
23
23
|
private constructor() {}
|
|
24
24
|
|
|
25
25
|
/** Get the default sampler configuration. */
|
|
26
|
-
static default():
|
|
26
|
+
static default(): SamplerConfig {
|
|
27
27
|
return samplerPresetDefault();
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** Create a sampler with top-k filtering only. */
|
|
31
|
-
static topK(topK: number):
|
|
31
|
+
static topK(topK: number): SamplerConfig {
|
|
32
32
|
return samplerPresetTopK(topK);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Create a sampler with nucleus (top-p) sampling. */
|
|
36
|
-
static topP(topP: number):
|
|
36
|
+
static topP(topP: number): SamplerConfig {
|
|
37
37
|
return samplerPresetTopP(topP);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** Create a greedy sampler (always picks most probable token). */
|
|
41
|
-
static greedy():
|
|
41
|
+
static greedy(): SamplerConfig {
|
|
42
42
|
return samplerPresetGreedy();
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** Create a sampler with temperature scaling. */
|
|
46
|
-
static temperature(temperature: number):
|
|
46
|
+
static temperature(temperature: number): SamplerConfig {
|
|
47
47
|
return samplerPresetTemperature(temperature);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** Create a DRY sampler preset to reduce repetition. */
|
|
51
|
-
static dry():
|
|
51
|
+
static dry(): SamplerConfig {
|
|
52
52
|
return samplerPresetDry();
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Create a sampler configured for JSON output generation. */
|
|
56
|
-
static json():
|
|
56
|
+
static json(): SamplerConfig {
|
|
57
57
|
return samplerPresetJson();
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** Create a sampler with a custom grammar constraint. */
|
|
61
|
-
static grammar(grammar: string):
|
|
61
|
+
static grammar(grammar: string): SamplerConfig {
|
|
62
62
|
return samplerPresetGrammar(grammar);
|
|
63
63
|
}
|
|
64
64
|
}
|
package/src/wrapper.ts
CHANGED
|
@@ -2,45 +2,37 @@
|
|
|
2
2
|
* NobodyWho — React Native bindings for local LLM inference.
|
|
3
3
|
*
|
|
4
4
|
* This is the public entry point. It re-exports the public API:
|
|
5
|
-
* - Wrapper classes: Chat, Tool, TokenStream
|
|
6
|
-
* - Direct re-exports:
|
|
7
|
-
* -
|
|
5
|
+
* - Wrapper classes: Model, Chat, Encoder, CrossEncoder, Tool, TokenStream, Prompt
|
|
6
|
+
* - Direct re-exports: SamplerBuilder, SamplerConfig, Role
|
|
7
|
+
* - Types: ChatMessage, Asset, ToolCall
|
|
8
|
+
* - Utilities: SamplerPresets, cosineSimilarity
|
|
8
9
|
*
|
|
9
10
|
* This file is NOT generated — it is safe to edit.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
// Wrapper classes
|
|
13
|
+
// Wrapper classes
|
|
14
|
+
export { Model } from "./model";
|
|
13
15
|
export { Chat } from "./chat";
|
|
16
|
+
export { Encoder } from "./encoder";
|
|
17
|
+
export { CrossEncoder } from "./cross_encoder";
|
|
14
18
|
export { Prompt } from "./prompt";
|
|
15
19
|
export { Tool } from "./tool";
|
|
16
20
|
export { TokenStream } from "./streaming";
|
|
17
21
|
|
|
22
|
+
// Message types
|
|
23
|
+
export type { ChatMessage } from "./message";
|
|
24
|
+
|
|
18
25
|
// Re-export types that don't need wrapping.
|
|
19
26
|
export {
|
|
20
|
-
Model,
|
|
21
|
-
Encoder,
|
|
22
|
-
CrossEncoder,
|
|
23
27
|
SamplerBuilder,
|
|
24
28
|
SamplerConfig,
|
|
25
29
|
Role,
|
|
26
|
-
Message,
|
|
27
|
-
Message_Tags,
|
|
28
|
-
PromptPart,
|
|
29
|
-
PromptPart_Tags,
|
|
30
30
|
cosineSimilarity,
|
|
31
|
-
loadModel,
|
|
32
|
-
uniffiInitAsync,
|
|
33
31
|
} from "./index";
|
|
34
32
|
|
|
35
33
|
export type {
|
|
36
|
-
ModelInterface,
|
|
37
|
-
EncoderInterface,
|
|
38
|
-
CrossEncoderInterface,
|
|
39
|
-
SamplerBuilderInterface,
|
|
40
|
-
SamplerConfigInterface,
|
|
41
34
|
Asset,
|
|
42
35
|
ToolCall,
|
|
43
|
-
ToolParameter,
|
|
44
36
|
} from "./index";
|
|
45
37
|
|
|
46
38
|
// Ergonomic wrapper additions.
|