UNPKG

@ratley/react-native-apple-foundation-models

Version:
254 lines 9.28 kB
import { Platform } from "react-native"; import AppleFoundationModelsModule from "./AppleFoundationModelsModule.ios"; import { TextGenerationError, toTextGenerationError } from "./errors"; const isAndroid = Platform.OS === "android"; export * from "./AppleFoundationModels.types"; export { default as AppleFoundationModelsView } from "./AppleFoundationModelsView"; export { isTextGenerationError, TextGenerationError, toTextGenerationError, } from "./errors"; export async function isTextModelAvailable() { if (isAndroid) { return false; } return AppleFoundationModelsModule.isTextModelAvailable(); } /** * Get precise availability of the on-device language model. * - When `status === "available"`, the model is ready to use. * - When `status === "unavailable"`, inspect `reasonCode` to choose a fallback UX: * - `deviceNotEligible`: hardware doesn’t support Apple Intelligence. * - `appleIntelligenceNotEnabled`: user has not enabled Apple Intelligence in Settings. * - `modelNotReady`: model is downloading or otherwise not yet ready. * - `unknown`: an unrecognized system-reported reason. * - `unsupported`: platform/OS does not support the on-device model. * * `reasonMessage` may include a human-readable explanation suitable for display or logging. */ export async function getTextModelAvailability() { if (isAndroid) { return { status: "unavailable", reasonCode: "unsupported", }; } try { const anyModule = AppleFoundationModelsModule; if (typeof anyModule.getTextModelAvailability === "function") { return await anyModule.getTextModelAvailability(); } } catch { } // Fallback using boolean const ok = await isTextModelAvailable(); return ok ? { status: "available" } : { status: "unavailable", reasonCode: "unsupported", }; } /** * Generate a single text response. Returns the text and the sessionId used. */ export async function generateText(options) { const prompt = options.prompt?.trim(); if (!prompt) { throw new Error("Prompt must be a non-empty string."); } if (isAndroid) { throw new TextGenerationError({ code: "ERR_TEXT_GENERATION_UNSUPPORTED", message: "Text generation is not supported on Android.", }); } const { instructions, temperature, maxOutputTokens, sessionId } = options; try { return await AppleFoundationModelsModule.generateText({ prompt, system: instructions?.trim(), temperature, maxOutputTokens, sessionId, }); } catch (error) { throw toTextGenerationError(error); } } export default AppleFoundationModelsModule; export { LLMSession } from "./LLMSession"; export { useLLMSession } from "./useLLMSession"; // Basic schema validator (subset) function validateJSONSchema(schema) { const t = schema ?.type; const allowed = ["string", "number", "boolean", "array", "object"]; if (!t || !allowed.includes(t)) { throw new Error("ERR_OBJECT_SCHEMA_INVALID"); } if (t === "array") { const s = schema; validateJSONSchema(s.items); } if (t === "object") { const s = schema; const props = s.properties; if (!props || typeof props !== "object") { throw new Error("ERR_OBJECT_SCHEMA_INVALID"); } for (const key of Object.keys(props)) { validateJSONSchema(props[key]); } } } // Minimal runtime validator for the decoded object vs schema function validateAgainstSchema(value, schema) { switch (schema.type) { case "string": if (typeof value !== "string") return false; if (schema.minLength != null && value.length < schema.minLength) return false; if (schema.maxLength != null && value.length > schema.maxLength) return false; if (schema.enum && !schema.enum.includes(value)) return false; return true; case "number": if (typeof value !== "number" || Number.isNaN(value)) return false; if (schema.minimum != null && value < schema.minimum) return false; if (schema.maximum != null && value > schema.maximum) return false; return true; case "boolean": return typeof value === "boolean"; case "array": { if (!Array.isArray(value)) return false; return value.every((v) => validateAgainstSchema(v, schema.items)); } case "object": { if (typeof value !== "object" || value == null || Array.isArray(value)) return false; const obj = value; const required = new Set(schema.required ?? []); for (const [k, s] of Object.entries(schema.properties)) { const present = Object.hasOwn(obj, k); if (!present) { if (required.has(k)) return false; continue; } if (!validateAgainstSchema(obj[k], s)) return false; } return true; } } } // generateObject: prompt model to produce JSON, then parse + validate /** * Generate a structured object matching `schema`. * Prefers native guided generation when available, otherwise falls back to * prompt-then-parse with runtime validation against the schema. */ export async function generateObject(options) { const prompt = options.prompt?.trim(); if (!prompt) { throw new Error("ERR_OBJECT_PROMPT_INVALID"); } validateJSONSchema(options.schema); // Ask the model to respond strictly with JSON conforming to the schema const guidance = ` You must return ONLY valid JSON that conforms to this schema. No prose. If a field is not derivable, return a sensible default or an empty value that fits the schema constraints. `; const base = options.instructions ? options.instructions.trim() : undefined; const system = [base, guidance.trim()] .filter((v) => typeof v === "string" && v.length > 0) .join("\n\n"); if (isAndroid) { const error = new Error("Structured generation is not supported on Android."); error.code = "ERR_OBJECT_GENERATION_UNSUPPORTED"; throw error; } // Prefer native guided generation if available const nativeSupported = typeof AppleFoundationModelsModule.generateObject === "function"; if (nativeSupported) { try { const { json, sessionId } = await AppleFoundationModelsModule.generateObject({ prompt, system: system || undefined, schema: JSON.stringify(options.schema), sessionId: options.sessionId, temperature: 0.2, maxOutputTokens: 512, }); const parsed = JSON.parse(json); if (!validateAgainstSchema(parsed, options.schema)) { const err = new Error("Model output does not match schema"); err.code = "ERR_OBJECT_GENERATION_DECODE_FAILED"; throw err; } return { object: parsed, sessionId }; } catch (error) { const e = error; if (typeof e === "object" && e && e.code === "ERR_TEXT_GENERATION_UNSUPPORTED") { // Fallback to text prompting } else { throw error; } } } const { text, sessionId } = await (async () => { try { return await generateText({ prompt, instructions: system || undefined, sessionId: options.sessionId, // keep temperature conservative for structure temperature: 0.2, maxOutputTokens: 512, }); } catch (error) { // Surface text error as object generation runtime const e = error; const message = typeof e === "object" && e && "message" in e && typeof e.message === "string" ? e.message : "Object generation failed"; const wrapped = new Error(message); wrapped.code = "ERR_OBJECT_GENERATION_RUNTIME"; throw wrapped; } })(); let parsed; try { parsed = JSON.parse(text); } catch (_parseError) { const err = new Error("Model did not return valid JSON"); err.code = "ERR_OBJECT_GENERATION_DECODE_FAILED"; throw err; } if (!validateAgainstSchema(parsed, options.schema)) { const err = new Error("Model output does not match schema"); err.code = "ERR_OBJECT_GENERATION_DECODE_FAILED"; throw err; } return { object: parsed, sessionId }; } //# sourceMappingURL=index.js.map