react-native-nobodywho 0.0.1 → 0.1.0

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.
@@ -0,0 +1,16 @@
1
+ // Generated by uniffi-bindgen-react-native
2
+ #ifdef __cplusplus
3
+ #import "react-native-nobodywho.h"
4
+ #endif
5
+
6
+ #ifdef RCT_NEW_ARCH_ENABLED
7
+ #import "RNNativeModuleSpec.h"
8
+
9
+ @interface Nobodywho : NSObject <NativeNobodywhoSpec>
10
+ #else
11
+ #import <React/RCTBridgeModule.h>
12
+
13
+ @interface Nobodywho : NSObject <RCTBridgeModule>
14
+ #endif
15
+
16
+ @end
@@ -0,0 +1,66 @@
1
+ // Generated by uniffi-bindgen-react-native
2
+ #import "Nobodywho.h"
3
+
4
+ namespace uniffi_generated {
5
+ using namespace facebook::react;
6
+ /**
7
+ * ObjC++ class for module 'NativeNobodywho'
8
+ */
9
+ class JSI_EXPORT NativeNobodywhoSpecJSI : public ObjCTurboModule {
10
+ public:
11
+ NativeNobodywhoSpecJSI(const ObjCTurboModule::InitParams &params);
12
+ std::shared_ptr<CallInvoker> callInvoker;
13
+ };
14
+
15
+ static facebook::jsi::Value __hostFunction_Nobodywho_installRustCrate(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
16
+ auto& tm = static_cast<NativeNobodywhoSpecJSI&>(turboModule);
17
+ auto jsInvoker = tm.callInvoker;
18
+ uint8_t result = nobodywho::installRustCrate(rt, jsInvoker);
19
+ return facebook::jsi::Value(rt, result);
20
+ }
21
+ static facebook::jsi::Value __hostFunction_Nobodywho_cleanupRustCrate(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
22
+ uint8_t result = nobodywho::cleanupRustCrate(rt);
23
+ return facebook::jsi::Value(rt, result);
24
+ }
25
+
26
+ NativeNobodywhoSpecJSI::NativeNobodywhoSpecJSI(const ObjCTurboModule::InitParams &params)
27
+ : ObjCTurboModule(params), callInvoker(params.jsInvoker) {
28
+ this->methodMap_["installRustCrate"] = MethodMetadata {1, __hostFunction_Nobodywho_installRustCrate};
29
+ this->methodMap_["cleanupRustCrate"] = MethodMetadata {1, __hostFunction_Nobodywho_cleanupRustCrate};
30
+ }
31
+ } // namespace uniffi_generated
32
+
33
+ @implementation Nobodywho
34
+ RCT_EXPORT_MODULE()
35
+
36
+ // Don't compile this code when we build for the old architecture.
37
+ #ifdef RCT_NEW_ARCH_ENABLED
38
+
39
+ // Automated testing checks nobodywho
40
+ // by comparing the whole line here.
41
+ /*
42
+ - (NSNumber *)multiply:(double)a b:(double)b {
43
+ NSNumber *result = @(nobodywho::multiply(a, b));
44
+ }
45
+ */
46
+
47
+ - (NSNumber *)installRustCrate {
48
+ @throw [NSException exceptionWithName:@"UnreachableException"
49
+ reason:@"This method should never be called."
50
+ userInfo:nil];
51
+ }
52
+
53
+ - (NSNumber *)cleanupRustCrate {
54
+ @throw [NSException exceptionWithName:@"UnreachableException"
55
+ reason:@"This method should never be called."
56
+ userInfo:nil];
57
+ }
58
+
59
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
60
+ (const facebook::react::ObjCTurboModule::InitParams &)params
61
+ {
62
+ return std::make_shared<uniffi_generated::NativeNobodywhoSpecJSI>(params);
63
+ }
64
+ #endif
65
+
66
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nobodywho",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Run LLMs locally with offline inference — React Native bindings",
5
5
  "main": "src/wrapper.ts",
6
6
  "types": "src/wrapper.ts",
@@ -21,7 +21,16 @@
21
21
  "offline"
22
22
  ],
23
23
  "files": [
24
- "README.md"
24
+ "src/",
25
+ "generated/",
26
+ "cpp/",
27
+ "ios/",
28
+ "android/",
29
+ "Nobodywho.podspec",
30
+ "ubrn.config.yaml",
31
+ "!android/build",
32
+ "!android/.cxx",
33
+ "!android/src/main/jniLibs"
25
34
  ],
26
35
  "codegenConfig": {
27
36
  "name": "NobodywhoSpec",
@@ -0,0 +1,10 @@
1
+ // Generated by uniffi-bindgen-react-native
2
+ import type { TurboModule } from 'react-native';
3
+ import { TurboModuleRegistry } from 'react-native';
4
+
5
+ export interface Spec extends TurboModule {
6
+ installRustCrate(): boolean;
7
+ cleanupRustCrate(): boolean;
8
+ }
9
+
10
+ export default TurboModuleRegistry.getEnforcing<Spec>('Nobodywho');
package/src/chat.ts ADDED
@@ -0,0 +1,155 @@
1
+ import {
2
+ RustChat,
3
+ loadModel,
4
+ type ModelInterface,
5
+ type SamplerConfigInterface,
6
+ type Message,
7
+ } from "../generated/ts/nobodywho";
8
+ import { TokenStream } from "./streaming";
9
+ import type { Prompt } from "./prompt";
10
+ import type { Tool } from "./tool";
11
+
12
+ /**
13
+ * A chat session for local LLM inference.
14
+ *
15
+ * Wraps the internal RustChat with an ergonomic API that uses
16
+ * the wrapper Tool and TokenStream types.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const model = await loadModel("model.gguf", true);
21
+ * const chat = new Chat({
22
+ * model,
23
+ * systemPrompt: "You are a helpful assistant.",
24
+ * });
25
+ * for await (const token of chat.ask("Hello!")) {
26
+ * process.stdout.write(token);
27
+ * }
28
+ * ```
29
+ */
30
+ export class Chat {
31
+ /** @internal */
32
+ private readonly _inner: RustChat;
33
+
34
+ constructor(opts: {
35
+ model: ModelInterface;
36
+ systemPrompt?: string;
37
+ contextSize?: number;
38
+ templateVariables?: Map<string, boolean>;
39
+ tools?: Tool[];
40
+ sampler?: SamplerConfigInterface;
41
+ }) {
42
+ this._inner = new RustChat(
43
+ opts.model,
44
+ opts.systemPrompt ?? undefined,
45
+ opts.contextSize ?? 4096,
46
+ opts.templateVariables ?? undefined,
47
+ opts.tools?.map((t) => t._inner) ?? undefined,
48
+ opts.sampler ?? undefined,
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Create a chat session directly from a model path.
54
+ * Loads the model and creates the chat in one step.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const chat = await Chat.fromPath({
59
+ * modelPath: "model.gguf",
60
+ * systemPrompt: "You are a helpful assistant.",
61
+ * });
62
+ * ```
63
+ */
64
+ static async fromPath(opts: {
65
+ modelPath: string;
66
+ useGpu?: boolean;
67
+ imageModelPath?: string;
68
+ systemPrompt?: string;
69
+ contextSize?: number;
70
+ templateVariables?: Map<string, boolean>;
71
+ tools?: Tool[];
72
+ sampler?: SamplerConfigInterface;
73
+ }): Promise<Chat> {
74
+ const model = await loadModel(
75
+ opts.modelPath,
76
+ opts.useGpu ?? true,
77
+ opts.imageModelPath,
78
+ );
79
+ return new Chat({ model, ...opts });
80
+ }
81
+
82
+ /** Send a text message or multimodal prompt and get a token stream for the response. */
83
+ ask(message: string | Prompt): TokenStream {
84
+ if (typeof message === "string") {
85
+ return new TokenStream(this._inner.ask(message));
86
+ }
87
+ return new TokenStream(this._inner.askWithPrompt(message._parts));
88
+ }
89
+
90
+ /** Stop the current generation. */
91
+ stopGeneration(): void {
92
+ this._inner.stopGeneration();
93
+ }
94
+
95
+ /** Reset the chat context with a new system prompt and tools. */
96
+ async resetContext(opts?: {
97
+ systemPrompt?: string;
98
+ tools?: Tool[];
99
+ }): Promise<void> {
100
+ return this._inner.resetContext(
101
+ opts?.systemPrompt ?? undefined,
102
+ opts?.tools?.map((t) => t._inner) ?? undefined,
103
+ );
104
+ }
105
+
106
+ /** Reset the chat history, keeping the system prompt and tools. */
107
+ async resetHistory(): Promise<void> {
108
+ return this._inner.resetHistory();
109
+ }
110
+
111
+ /** Get the current chat history as a list of messages. */
112
+ async getChatHistory(): Promise<Message[]> {
113
+ return this._inner.getChatHistory();
114
+ }
115
+
116
+ /** Set the chat history from a list of messages. */
117
+ async setChatHistory(messages: Message[]): Promise<void> {
118
+ return this._inner.setChatHistory(messages);
119
+ }
120
+
121
+ /** Get the current system prompt. */
122
+ async getSystemPrompt(): Promise<string | undefined> {
123
+ return this._inner.getSystemPrompt();
124
+ }
125
+
126
+ /** Set the system prompt. */
127
+ async setSystemPrompt(systemPrompt: string | undefined): Promise<void> {
128
+ return this._inner.setSystemPrompt(systemPrompt);
129
+ }
130
+
131
+ /** Set the tools available to the model. */
132
+ async setTools(tools: Tool[]): Promise<void> {
133
+ return this._inner.setTools(tools.map((t) => t._inner));
134
+ }
135
+
136
+ /** Set a template variable. */
137
+ async setTemplateVariable(name: string, value: boolean): Promise<void> {
138
+ return this._inner.setTemplateVariable(name, value);
139
+ }
140
+
141
+ /** Get all template variables. */
142
+ async getTemplateVariables(): Promise<Map<string, boolean>> {
143
+ return this._inner.getTemplateVariables();
144
+ }
145
+
146
+ /** Set the sampler configuration. */
147
+ async setSamplerConfig(sampler: SamplerConfigInterface): Promise<void> {
148
+ return this._inner.setSamplerConfig(sampler);
149
+ }
150
+
151
+ /** Get the current sampler configuration as a JSON string. */
152
+ async getSamplerConfigJson(): Promise<string> {
153
+ return this._inner.getSamplerConfigJson();
154
+ }
155
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,41 @@
1
+ // Generated by uniffi-bindgen-react-native
2
+ import installer from './NativeNobodywho';
3
+
4
+ // Register the rust crate with Hermes
5
+ // - the boolean flag ensures this loads exactly once, even if the JS
6
+ // code is reloaded (e.g. during development with metro).
7
+ let rustInstalled = false;
8
+ if (!rustInstalled) {
9
+ installer.installRustCrate();
10
+ rustInstalled = true;
11
+ }
12
+
13
+ // Export the generated bindings to the app.
14
+ export * from './../generated/ts/nobodywho';
15
+
16
+ // Now import the bindings so we can:
17
+ // - intialize them
18
+ // - export them as namespaced objects as the default export.
19
+ import * as nobodywho from './../generated/ts/nobodywho';
20
+
21
+ // Initialize the generated bindings: mostly checksums, but also callbacks.
22
+ // - the boolean flag ensures this loads exactly once, even if the JS code
23
+ // is reloaded (e.g. during development with metro).
24
+ let initialized = false;
25
+ if (!initialized) {
26
+ nobodywho.default.initialize();
27
+ initialized = true;
28
+ }
29
+
30
+ // This provides parity with the index.web.ts version of this file.
31
+ // The web version relies on an asynchronous fetch, which this doesn't
32
+ // need, so we just no-op.
33
+ export async function uniffiInitAsync() {
34
+ // NOOP.
35
+ }
36
+
37
+ // Export the crates as individually namespaced objects.
38
+ export default {
39
+ nobodywho,
40
+ };
41
+
package/src/prompt.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { PromptPart } from "../generated/ts/nobodywho";
2
+
3
+ /** A text part of a multimodal prompt. */
4
+ class TextPart {
5
+ /** @internal */
6
+ readonly _inner: PromptPart;
7
+
8
+ constructor(content: string) {
9
+ this._inner = new PromptPart.Text({ content });
10
+ }
11
+ }
12
+
13
+ /** An image part of a multimodal prompt. */
14
+ class ImagePart {
15
+ /** @internal */
16
+ readonly _inner: PromptPart;
17
+
18
+ constructor(path: string) {
19
+ this._inner = new PromptPart.Image({ path });
20
+ }
21
+ }
22
+
23
+ /** An audio part of a multimodal prompt. */
24
+ class AudioPart {
25
+ /** @internal */
26
+ readonly _inner: PromptPart;
27
+
28
+ constructor(path: string) {
29
+ this._inner = new PromptPart.Audio({ path });
30
+ }
31
+ }
32
+
33
+ type Part = TextPart | ImagePart | AudioPart;
34
+
35
+ /**
36
+ * A multimodal prompt composed of text, image, and audio parts.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const prompt = new Prompt([
41
+ * Prompt.Text("Tell me what you see in the first image."),
42
+ * Prompt.Image("./dog.png"),
43
+ * Prompt.Text("Also tell me what you see in the second image."),
44
+ * Prompt.Image("./penguin.png"),
45
+ * ]);
46
+ *
47
+ * const stream = chat.ask(prompt);
48
+ * ```
49
+ */
50
+ export class Prompt {
51
+ /** @internal */
52
+ readonly _parts: PromptPart[];
53
+
54
+ constructor(parts: Part[]) {
55
+ this._parts = parts.map((p) => p._inner);
56
+ }
57
+
58
+ /** Create a text part. */
59
+ static Text(content: string): TextPart {
60
+ return new TextPart(content);
61
+ }
62
+
63
+ /** Create an image part from a file path. */
64
+ static Image(path: string): ImagePart {
65
+ return new ImagePart(path);
66
+ }
67
+
68
+ /** Create an audio part from a file path. */
69
+ static Audio(path: string): AudioPart {
70
+ return new AudioPart(path);
71
+ }
72
+ }
@@ -0,0 +1,64 @@
1
+ import type { SamplerConfigInterface } from "../generated/ts/nobodywho";
2
+ import {
3
+ samplerPresetDefault,
4
+ samplerPresetDry,
5
+ samplerPresetGrammar,
6
+ samplerPresetGreedy,
7
+ samplerPresetJson,
8
+ samplerPresetTemperature,
9
+ samplerPresetTopK,
10
+ samplerPresetTopP,
11
+ } from "../generated/ts/nobodywho";
12
+
13
+ /**
14
+ * Static factory methods for common sampler configurations.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const sampler = SamplerPresets.temperature(0.7);
19
+ * const chat = new Chat(model, undefined, 4096, undefined, undefined, sampler);
20
+ * ```
21
+ */
22
+ export class SamplerPresets {
23
+ private constructor() {}
24
+
25
+ /** Get the default sampler configuration. */
26
+ static default(): SamplerConfigInterface {
27
+ return samplerPresetDefault();
28
+ }
29
+
30
+ /** Create a sampler with top-k filtering only. */
31
+ static topK(topK: number): SamplerConfigInterface {
32
+ return samplerPresetTopK(topK);
33
+ }
34
+
35
+ /** Create a sampler with nucleus (top-p) sampling. */
36
+ static topP(topP: number): SamplerConfigInterface {
37
+ return samplerPresetTopP(topP);
38
+ }
39
+
40
+ /** Create a greedy sampler (always picks most probable token). */
41
+ static greedy(): SamplerConfigInterface {
42
+ return samplerPresetGreedy();
43
+ }
44
+
45
+ /** Create a sampler with temperature scaling. */
46
+ static temperature(temperature: number): SamplerConfigInterface {
47
+ return samplerPresetTemperature(temperature);
48
+ }
49
+
50
+ /** Create a DRY sampler preset to reduce repetition. */
51
+ static dry(): SamplerConfigInterface {
52
+ return samplerPresetDry();
53
+ }
54
+
55
+ /** Create a sampler configured for JSON output generation. */
56
+ static json(): SamplerConfigInterface {
57
+ return samplerPresetJson();
58
+ }
59
+
60
+ /** Create a sampler with a custom grammar constraint. */
61
+ static grammar(grammar: string): SamplerConfigInterface {
62
+ return samplerPresetGrammar(grammar);
63
+ }
64
+ }
@@ -0,0 +1,46 @@
1
+ import type { RustTokenStreamInterface } from "../generated/ts/nobodywho";
2
+
3
+ /**
4
+ * A stream of response tokens from the model.
5
+ *
6
+ * Wraps the internal RustTokenStream and provides an async iterable interface
7
+ * so it can be used with `for await`.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const stream = chat.ask("Hello!");
12
+ * for await (const token of stream) {
13
+ * process.stdout.write(token);
14
+ * }
15
+ * // Or get the complete response:
16
+ * const stream = chat.ask("Hello!");
17
+ * const fullResponse = await stream.completed();
18
+ * ```
19
+ */
20
+ export class TokenStream implements AsyncIterable<string> {
21
+ /** @internal */
22
+ readonly _inner: RustTokenStreamInterface;
23
+
24
+ /** @internal */
25
+ constructor(inner: RustTokenStreamInterface) {
26
+ this._inner = inner;
27
+ }
28
+
29
+ /** Get the next token. Returns undefined when generation is complete. */
30
+ async nextToken(): Promise<string | undefined> {
31
+ return this._inner.nextToken();
32
+ }
33
+
34
+ /** Wait for the full response to complete and return it. */
35
+ async completed(): Promise<string> {
36
+ return this._inner.completed();
37
+ }
38
+
39
+ /** Async iterator support for `for await (const token of stream)`. */
40
+ async *[Symbol.asyncIterator](): AsyncIterableIterator<string> {
41
+ let token: string | undefined;
42
+ while ((token = await this._inner.nextToken()) !== undefined) {
43
+ yield token;
44
+ }
45
+ }
46
+ }
package/src/tool.ts ADDED
@@ -0,0 +1,80 @@
1
+ import {
2
+ RustTool,
3
+ type RustToolCallback,
4
+ type ToolParameter,
5
+ } from "../generated/ts/nobodywho";
6
+
7
+ /**
8
+ * Convert a value from JSON based on the JSON Schema type.
9
+ */
10
+ function convertValue(
11
+ value: unknown,
12
+ schema: Record<string, unknown>,
13
+ ): unknown {
14
+ switch (schema.type) {
15
+ case "integer":
16
+ case "number":
17
+ return Number(value);
18
+ case "boolean":
19
+ return typeof value === "string" ? value === "true" : Boolean(value);
20
+ case "string":
21
+ return String(value);
22
+ default:
23
+ return value;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * A tool that the model can call during inference.
29
+ *
30
+ * Wraps the internal RustTool with an ergonomic API that handles
31
+ * JSON parsing and typed argument dispatch.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const weatherTool = new Tool({
36
+ * name: "get_weather",
37
+ * description: "Get the current weather for a city",
38
+ * parameters: {
39
+ * city: { type: "string" },
40
+ * unit: { type: "string", enum: ["celsius", "fahrenheit"] },
41
+ * },
42
+ * call: ({ city, unit }) => JSON.stringify({ temp: 22, unit }),
43
+ * });
44
+ * ```
45
+ */
46
+ export class Tool {
47
+ /** @internal */
48
+ readonly _inner: RustTool;
49
+
50
+ constructor(opts: {
51
+ name: string;
52
+ description: string;
53
+ parameters: Record<string, Record<string, unknown>>;
54
+ call: (args: Record<string, unknown>) => string;
55
+ }) {
56
+ const paramEntries = Object.entries(opts.parameters);
57
+
58
+ const callback: RustToolCallback = {
59
+ call(argumentsJson: string): string {
60
+ const parsed = JSON.parse(argumentsJson);
61
+ const args: Record<string, unknown> = {};
62
+ for (const [paramName, schema] of paramEntries) {
63
+ args[paramName] = convertValue(parsed[paramName], schema);
64
+ }
65
+ return opts.call(args);
66
+ },
67
+ };
68
+
69
+ const toolParams: ToolParameter[] = paramEntries.map(
70
+ ([name, schema]) => ({ name, schema: JSON.stringify(schema) }),
71
+ );
72
+
73
+ this._inner = new RustTool(opts.name, opts.description, toolParams, callback);
74
+ }
75
+
76
+ /** Get the JSON schema for this tool's parameters. */
77
+ getSchemaJson(): string {
78
+ return this._inner.getSchemaJson();
79
+ }
80
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ rust:
3
+ directory: ..
4
+ manifestPath: uniffi/Cargo.toml
5
+
6
+ bindings:
7
+ cpp: generated/cpp
8
+ ts: generated/ts
9
+
10
+ turboModule:
11
+ cpp: cpp
12
+ ts: src