From 4f3992c5685ae3a0951ca93f0df9ae9504edb525 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Mon, 13 Jan 2025 17:09:22 -0800 Subject: [PATCH 01/12] refactor: clean up bindings dir and move tests around --- .../hf-transformers/bindings/all_inmemory.ts | 17 --------- .../hf-transformers/bindings/all_sqlite.ts | 36 ------------------ .../{local_hf.ts => registerTasks.ts} | 0 .../src/hf-transformers/browser.ts | 3 +- .../test/HFTransformersBinding.test.ts | 37 +++++++++++++++++++ .../src/tf-mediapipe/bindings/all_inmemory.ts | 19 ---------- .../src/tf-mediapipe/bindings/all_sqlite.ts | 36 ------------------ .../{local_mp.ts => registerTasks.ts} | 0 .../ai-provider/src/tf-mediapipe/browser.ts | 3 +- .../src/tf-mediapipe/test/TfMediaPipe.test.ts | 37 +++++++++++++++++++ packages/storage/package.json | 6 +-- .../inmemory/test/InMemoryJobQueue.test.ts} | 7 ++-- .../test/InMemoryTaskGraphRepository.test.ts | 2 +- .../test/InMemoryTaskOutputRepository.test.ts | 2 +- ...-Sqlite.test.ts => SqliteJobQueue.test.ts} | 0 15 files changed, 84 insertions(+), 121 deletions(-) delete mode 100644 packages/ai-provider/src/hf-transformers/bindings/all_inmemory.ts delete mode 100644 packages/ai-provider/src/hf-transformers/bindings/all_sqlite.ts rename packages/ai-provider/src/hf-transformers/bindings/{local_hf.ts => registerTasks.ts} (100%) create mode 100644 packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts delete mode 100644 packages/ai-provider/src/tf-mediapipe/bindings/all_inmemory.ts delete mode 100644 packages/ai-provider/src/tf-mediapipe/bindings/all_sqlite.ts rename packages/ai-provider/src/tf-mediapipe/bindings/{local_mp.ts => registerTasks.ts} (100%) create mode 100644 packages/ai-provider/src/tf-mediapipe/test/TfMediaPipe.test.ts rename packages/{core/src/job/test/JobQueue-InMemory.test.ts => storage/src/browser/inmemory/test/InMemoryJobQueue.test.ts} (96%) rename packages/storage/src/bun/sqlite/test/{JobQueue-Sqlite.test.ts => SqliteJobQueue.test.ts} (100%) diff --git a/packages/ai-provider/src/hf-transformers/bindings/all_inmemory.ts b/packages/ai-provider/src/hf-transformers/bindings/all_inmemory.ts deleted file mode 100644 index fec0069..0000000 --- a/packages/ai-provider/src/hf-transformers/bindings/all_inmemory.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ConcurrencyLimiter, TaskInput, TaskOutput } from "ellmers-core"; -import { getProviderRegistry, ModelProcessorEnum } from "ellmers-ai"; -import { InMemoryJobQueue } from "ellmers-storage/inmemory"; -import { registerHuggingfaceLocalTasks } from "./local_hf"; -import "../model/ONNXModelSamples"; - -export async function registerHuggingfaceLocalTasksInMemory() { - registerHuggingfaceLocalTasks(); - const ProviderRegistry = getProviderRegistry(); - const jobQueue = new InMemoryJobQueue( - "local_hf", - new ConcurrencyLimiter(1, 10), - 10 - ); - ProviderRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); - jobQueue.start(); -} diff --git a/packages/ai-provider/src/hf-transformers/bindings/all_sqlite.ts b/packages/ai-provider/src/hf-transformers/bindings/all_sqlite.ts deleted file mode 100644 index 3535d37..0000000 --- a/packages/ai-provider/src/hf-transformers/bindings/all_sqlite.ts +++ /dev/null @@ -1,36 +0,0 @@ -// import { registerHuggingfaceLocalTasks } from "./local_hf"; -// import { registerMediaPipeTfJsLocalTasks } from "./local_mp"; -// import { getProviderRegistry } from "../provider/ProviderRegistry"; -// import { ModelProcessorEnum } from "../model/Model"; -// import { ConcurrencyLimiter } from "../job/ConcurrencyLimiter"; -// import { SqliteJobQueue } from "../job/SqliteJobQueue"; -// import { getDatabase } from "../util/db_sqlite"; -// import { TaskInput, TaskOutput } from "../task/base/Task"; -// import { mkdirSync } from "node:fs"; - -// mkdirSync("./.cache", { recursive: true }); -// const db = getDatabase("./.cache/local.db"); - -// export async function registerHuggingfaceLocalTasksSqlite() { -// registerHuggingfaceLocalTasks(); -// const ProviderRegistry = getProviderRegistry(); -// const jobQueue = new SqliteJobQueue( -// db, -// "local_hf", -// new ConcurrencyLimiter(1, 10) -// ); -// ProviderRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); -// jobQueue.start(); -// } - -// export async function registerMediaPipeTfJsLocalSqlite() { -// registerMediaPipeTfJsLocalTasks(); -// const ProviderRegistry = getProviderRegistry(); -// const jobQueue = new SqliteJobQueue( -// db, -// "local_media_pipe", -// new ConcurrencyLimiter(1, 10) -// ); -// ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); -// jobQueue.start(); -// } diff --git a/packages/ai-provider/src/hf-transformers/bindings/local_hf.ts b/packages/ai-provider/src/hf-transformers/bindings/registerTasks.ts similarity index 100% rename from packages/ai-provider/src/hf-transformers/bindings/local_hf.ts rename to packages/ai-provider/src/hf-transformers/bindings/registerTasks.ts diff --git a/packages/ai-provider/src/hf-transformers/browser.ts b/packages/ai-provider/src/hf-transformers/browser.ts index b9657be..caa4d0b 100644 --- a/packages/ai-provider/src/hf-transformers/browser.ts +++ b/packages/ai-provider/src/hf-transformers/browser.ts @@ -7,5 +7,4 @@ export * from "./provider/HuggingFaceLocal_TaskRun"; export * from "./model/ONNXTransformerJsModel"; -export * from "./bindings/local_hf"; -export * from "./bindings/all_inmemory"; +export * from "./bindings/registerTasks"; diff --git a/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts b/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts new file mode 100644 index 0000000..749cba2 --- /dev/null +++ b/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts @@ -0,0 +1,37 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { describe, expect, it } from "bun:test"; +import { ConcurrencyLimiter, TaskInput, TaskOutput } from "ellmers-core"; +import { getProviderRegistry, ModelProcessorEnum } from "ellmers-ai"; +import { InMemoryJobQueue } from "ellmers-storage/inmemory"; +import { registerHuggingfaceLocalTasks } from "../bindings/registerTasks"; +import "../model/ONNXModelSamples"; + +const HFQUEUE = "local_hf"; + +export async function registerHuggingfaceLocalTasksInMemory() { + registerHuggingfaceLocalTasks(); + const providerRegistry = getProviderRegistry(); + const jobQueue = new InMemoryJobQueue( + HFQUEUE, + new ConcurrencyLimiter(1, 10), + 10 + ); + providerRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); + jobQueue.start(); + return providerRegistry; +} + +describe("HFTransformersBinding.", () => { + it("should not fail", async () => { + const providerRegistry = await registerHuggingfaceLocalTasksInMemory(); + const queue = providerRegistry.getQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS); + expect(queue).toBeDefined(); + expect(queue?.queue).toEqual(HFQUEUE); + }); +}); diff --git a/packages/ai-provider/src/tf-mediapipe/bindings/all_inmemory.ts b/packages/ai-provider/src/tf-mediapipe/bindings/all_inmemory.ts deleted file mode 100644 index c1929fe..0000000 --- a/packages/ai-provider/src/tf-mediapipe/bindings/all_inmemory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { getProviderRegistry } from "ellmers-ai"; -import { InMemoryJobQueue } from "ellmers-storage/inmemory"; -import { ModelProcessorEnum } from "ellmers-ai"; -import { ConcurrencyLimiter } from "ellmers-core"; -import { TaskInput, TaskOutput } from "ellmers-core"; -import { registerMediaPipeTfJsLocalTasks } from "./local_mp"; -import "../model/MediaPipeModelSamples"; - -export async function registerMediaPipeTfJsLocalInMemory() { - registerMediaPipeTfJsLocalTasks(); - const ProviderRegistry = getProviderRegistry(); - const jobQueue = new InMemoryJobQueue( - "local_media_pipe", - new ConcurrencyLimiter(1, 10), - 10 - ); - ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); - jobQueue.start(); -} diff --git a/packages/ai-provider/src/tf-mediapipe/bindings/all_sqlite.ts b/packages/ai-provider/src/tf-mediapipe/bindings/all_sqlite.ts deleted file mode 100644 index 3535d37..0000000 --- a/packages/ai-provider/src/tf-mediapipe/bindings/all_sqlite.ts +++ /dev/null @@ -1,36 +0,0 @@ -// import { registerHuggingfaceLocalTasks } from "./local_hf"; -// import { registerMediaPipeTfJsLocalTasks } from "./local_mp"; -// import { getProviderRegistry } from "../provider/ProviderRegistry"; -// import { ModelProcessorEnum } from "../model/Model"; -// import { ConcurrencyLimiter } from "../job/ConcurrencyLimiter"; -// import { SqliteJobQueue } from "../job/SqliteJobQueue"; -// import { getDatabase } from "../util/db_sqlite"; -// import { TaskInput, TaskOutput } from "../task/base/Task"; -// import { mkdirSync } from "node:fs"; - -// mkdirSync("./.cache", { recursive: true }); -// const db = getDatabase("./.cache/local.db"); - -// export async function registerHuggingfaceLocalTasksSqlite() { -// registerHuggingfaceLocalTasks(); -// const ProviderRegistry = getProviderRegistry(); -// const jobQueue = new SqliteJobQueue( -// db, -// "local_hf", -// new ConcurrencyLimiter(1, 10) -// ); -// ProviderRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); -// jobQueue.start(); -// } - -// export async function registerMediaPipeTfJsLocalSqlite() { -// registerMediaPipeTfJsLocalTasks(); -// const ProviderRegistry = getProviderRegistry(); -// const jobQueue = new SqliteJobQueue( -// db, -// "local_media_pipe", -// new ConcurrencyLimiter(1, 10) -// ); -// ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); -// jobQueue.start(); -// } diff --git a/packages/ai-provider/src/tf-mediapipe/bindings/local_mp.ts b/packages/ai-provider/src/tf-mediapipe/bindings/registerTasks.ts similarity index 100% rename from packages/ai-provider/src/tf-mediapipe/bindings/local_mp.ts rename to packages/ai-provider/src/tf-mediapipe/bindings/registerTasks.ts diff --git a/packages/ai-provider/src/tf-mediapipe/browser.ts b/packages/ai-provider/src/tf-mediapipe/browser.ts index ad31311..6fc38a1 100644 --- a/packages/ai-provider/src/tf-mediapipe/browser.ts +++ b/packages/ai-provider/src/tf-mediapipe/browser.ts @@ -7,5 +7,4 @@ export * from "./provider/MediaPipeLocalTaskRun"; export * from "./model/MediaPipeModel"; -export * from "./bindings/local_mp"; -export * from "./bindings/all_inmemory"; +export * from "./bindings/registerTasks"; diff --git a/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipe.test.ts b/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipe.test.ts new file mode 100644 index 0000000..9651f21 --- /dev/null +++ b/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipe.test.ts @@ -0,0 +1,37 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { describe, expect, it } from "bun:test"; +import { ConcurrencyLimiter, TaskInput, TaskOutput } from "ellmers-core"; +import { getProviderRegistry, ModelProcessorEnum } from "ellmers-ai"; +import { InMemoryJobQueue } from "ellmers-storage/inmemory"; +import { registerMediaPipeTfJsLocalTasks } from "../bindings/registerTasks"; +import "../model/MediaPipeModelSamples"; + +const TFQUEUE = "local_tf-mediapipe"; + +export async function registerMediaPipeTfJsLocalInMemory() { + registerMediaPipeTfJsLocalTasks(); + const ProviderRegistry = getProviderRegistry(); + const jobQueue = new InMemoryJobQueue( + TFQUEUE, + new ConcurrencyLimiter(1, 10), + 10 + ); + ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); + jobQueue.start(); + return ProviderRegistry; +} + +describe("TfMediaPipe.", () => { + it("should not fail", async () => { + const providerRegistry = await registerMediaPipeTfJsLocalInMemory(); + const queue = providerRegistry.getQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL); + expect(queue).toBeDefined(); + expect(queue?.queue).toEqual(TFQUEUE); + }); +}); diff --git a/packages/storage/package.json b/packages/storage/package.json index cc3da7e..d95a99e 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -5,9 +5,9 @@ "description": "Ellmers is a tool for building and running DAG pipelines of AI tasks.", "scripts": { "watch": "concurrently -c 'auto' 'bun:watch-*'", - "watch-browser": "bun build --watch --no-clear-screen--target=browser --sourcemap=external --external ellmers-core --outdir ./dist/browser ./src/browser/*/index.ts", - "watch-node": "bun build --watch --no-clear-screen--target=node --sourcemap=external --external ellmers-core --outdir ./dist ./src/node/*/index.ts", - "watch-bun": "bun build --watch --no-clear-screen--target=bun --sourcemap=external --external ellmers-core --outdir ./dist ./src/bun/*/index.ts", + "watch-browser": "bun build --watch --no-clear-screen --target=browser --sourcemap=external --external ellmers-core --outdir ./dist/browser ./src/browser/*/index.ts", + "watch-node": "bun build --watch --no-clear-screen --target=node --sourcemap=external --external ellmers-core --outdir ./dist ./src/node/*/index.ts", + "watch-bun": "bun build --watch --no-clear-screen --target=bun --sourcemap=external --external ellmers-core --outdir ./dist ./src/bun/*/index.ts", "watch-types": "tsc --watch --preserveWatchOutput", "build": "bun run build-clean && bun run build-types && bun run build-browser && bun run build-node && bun run build-bun", "build-clean": "rm -fr dist/* tsconfig.tsbuildinfo", diff --git a/packages/core/src/job/test/JobQueue-InMemory.test.ts b/packages/storage/src/browser/inmemory/test/InMemoryJobQueue.test.ts similarity index 96% rename from packages/core/src/job/test/JobQueue-InMemory.test.ts rename to packages/storage/src/browser/inmemory/test/InMemoryJobQueue.test.ts index 26eb4a4..857c745 100644 --- a/packages/core/src/job/test/JobQueue-InMemory.test.ts +++ b/packages/storage/src/browser/inmemory/test/InMemoryJobQueue.test.ts @@ -6,10 +6,9 @@ // ******************************************************************************* import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test"; -import { Job, JobStatus } from "../base/Job"; +import { Job, JobStatus, TaskInput, TaskOutput } from "ellmers-core"; import { InMemoryJobQueue, InMemoryRateLimiter } from "ellmers-storage/inmemory"; -import { sleep } from "../../util/Misc"; -import { TaskInput, TaskOutput } from "../../task/base/Task"; +import { sleep } from "ellmers-core"; class TestJob extends Job { public async execute() { @@ -17,7 +16,7 @@ class TestJob extends Job { } } -describe("LocalJobQueue", () => { +describe("InMemoryJobQueue", () => { let jobQueue: InMemoryJobQueue; beforeEach(() => { diff --git a/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts b/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts index 92fb8ec..c18b483 100644 --- a/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts +++ b/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts @@ -8,7 +8,7 @@ import { describe, expect, it, beforeEach } from "bun:test"; import { rmdirSync } from "fs"; import { SingleTask, TaskOutput, DataFlow, TaskGraph, TaskRegistry } from "ellmers-core"; -import { InMemoryTaskGraphRepository } from "../InMemoryTaskGraphRepository"; +import { InMemoryTaskGraphRepository } from "ellmers-storage/inmemory"; class TestTask extends SingleTask { static readonly type = "TestTask"; diff --git a/packages/storage/src/browser/inmemory/test/InMemoryTaskOutputRepository.test.ts b/packages/storage/src/browser/inmemory/test/InMemoryTaskOutputRepository.test.ts index 8fcfb29..3144c23 100644 --- a/packages/storage/src/browser/inmemory/test/InMemoryTaskOutputRepository.test.ts +++ b/packages/storage/src/browser/inmemory/test/InMemoryTaskOutputRepository.test.ts @@ -6,8 +6,8 @@ // ******************************************************************************* import { describe, expect, it, beforeEach } from "bun:test"; -import { InMemoryTaskOutputRepository } from "../InMemoryTaskOutputRepository"; import { TaskInput, TaskOutput } from "ellmers-core"; +import { InMemoryTaskOutputRepository } from "ellmers-storage/inmemory"; describe("InMemoryTaskOutputRepository", () => { let repository: InMemoryTaskOutputRepository; diff --git a/packages/storage/src/bun/sqlite/test/JobQueue-Sqlite.test.ts b/packages/storage/src/bun/sqlite/test/SqliteJobQueue.test.ts similarity index 100% rename from packages/storage/src/bun/sqlite/test/JobQueue-Sqlite.test.ts rename to packages/storage/src/bun/sqlite/test/SqliteJobQueue.test.ts From 543c04e5b2509976038b00121981bcf640518c11 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Mon, 13 Jan 2025 21:41:01 -0800 Subject: [PATCH 02/12] refactor: move out sample data in prep for real model storage, and update some tests --- bun.lockb | Bin 232740 -> 233284 bytes docs/developers/01_getting_started.md | 10 +- examples/cli/src/ellmers.ts | 7 +- examples/web/src/App.tsx | 34 ++++++- examples/web/src/RunGraphFlow.tsx | 5 - package.json | 3 +- .../test/HFTransformersBinding.test.ts | 84 ++++++++++++----- .../src/tf-mediapipe/test/TfMediaPipe.test.ts | 37 -------- .../test/TfMediaPipeBinding.test.ts | 88 ++++++++++++++++++ packages/core/src/job/base/JobQueue.ts | 3 + .../src/browser/inmemory/InMemoryJobQueue.ts | 7 +- .../storage/src/bun/sqlite/SqliteJobQueue.ts | 6 +- .../bun/sqlite/test/SqliteJobQueue.test.ts | 4 +- packages/test/package.json | 32 +++++++ packages/test/src/index.ts | 32 +++++++ .../src/sample}/MediaPipeModelSamples.ts | 2 +- .../src/sample}/ONNXModelSamples.ts | 8 +- packages/test/src/util/db_sqlite.ts | 21 +++++ packages/test/tsconfig.json | 24 +++++ 19 files changed, 325 insertions(+), 82 deletions(-) delete mode 100644 packages/ai-provider/src/tf-mediapipe/test/TfMediaPipe.test.ts create mode 100644 packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts create mode 100644 packages/test/package.json create mode 100644 packages/test/src/index.ts rename packages/{ai-provider/src/tf-mediapipe/model => test/src/sample}/MediaPipeModelSamples.ts (86%) rename packages/{ai-provider/src/hf-transformers/model => test/src/sample}/ONNXModelSamples.ts (92%) create mode 100644 packages/test/src/util/db_sqlite.ts create mode 100644 packages/test/tsconfig.json diff --git a/bun.lockb b/bun.lockb index 45369b4dff5e0a770d1bbd6d56676ede797094fd..82c165b9a0b17f9930346592118d6c11b485dc96 100755 GIT binary patch delta 33619 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 ZYU@@ptCiT^g(iGn-CzF0!ip40hHbs9YPOP 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 diff --git a/docs/developers/01_getting_started.md b/docs/developers/01_getting_started.md index f5cd725..7861f2d 100644 --- a/docs/developers/01_getting_started.md +++ b/docs/developers/01_getting_started.md @@ -22,9 +22,9 @@ - [`packages/storage`](#packagesstorage) - [`packages/ai`](#packagesai) - [`packages/ai-provider`](#packagesai-provider) - - [`examples/cli`](#samplescli) - - [`examples/web`](#samplesweb) - - [`examples/ngraph`](#samplesngraph) + - [`examples/cli`](#examplescli) + - [`examples/web`](#examplesweb) + - [`examples/ngraph`](#examplesngraph) # Developer Getting Started @@ -51,7 +51,7 @@ After this, plese read [Architecture](02_architecture.md) before attempting to [ ```ts import { TaskGraphBuilder } from "ellmers-core"; -import { registerHuggingfaceLocalTasksInMemory } from "ellmers-ai-provider/hf-transformers/server"; +import { registerHuggingfaceLocalTasksInMemory } from "ellmers-test"; // config and start up registerHuggingfaceLocalTasksInMemory(); @@ -79,8 +79,8 @@ import { DataFlow, TaskGraph, TaskGraphRunner, - registerHuggingfaceLocalTasksInMemory, } from "ellmers-core"; +import { registerHuggingfaceLocalTasksInMemory } from "ellmers-test"; // config and start up registerHuggingfaceLocalTasksInMemory(); diff --git a/examples/cli/src/ellmers.ts b/examples/cli/src/ellmers.ts index b16a53a..b164249 100755 --- a/examples/cli/src/ellmers.ts +++ b/examples/cli/src/ellmers.ts @@ -4,8 +4,11 @@ import { program } from "commander"; import { argv } from "process"; import { AddBaseCommands } from "./TaskCLI"; import { getProviderRegistry } from "ellmers-ai"; -import { registerHuggingfaceLocalTasksInMemory } from "ellmers-ai-provider/hf-transformers/server"; -import { registerMediaPipeTfJsLocalInMemory } from "ellmers-ai-provider/tf-mediapipe/server"; +import { + registerHuggingfaceLocalTasksInMemory, + registerMediaPipeTfJsLocalInMemory, +} from "ellmers-test"; +import "ellmers-test"; program.version("1.0.0").description("A CLI to run Ellmers."); diff --git a/examples/web/src/App.tsx b/examples/web/src/App.tsx index f84fc50..4a9b5b2 100644 --- a/examples/web/src/App.tsx +++ b/examples/web/src/App.tsx @@ -2,8 +2,17 @@ import React, { useCallback, useEffect, useState } from "react"; import { ReactFlowProvider } from "@xyflow/react"; import { RunGraphFlow } from "./RunGraphFlow"; import { JsonEditor } from "./JsonEditor"; -import { JsonTask, JsonTaskItem, TaskGraph, TaskGraphBuilder } from "ellmers-core"; import { + ConcurrencyLimiter, + JsonTask, + JsonTaskItem, + TaskGraph, + TaskGraphBuilder, + TaskInput, + TaskOutput, +} from "ellmers-core"; +import { + IndexedDbQueue, IndexedDbTaskGraphRepository, IndexedDbTaskOutputRepository, } from "ellmers-storage/browser/indexeddb"; @@ -11,10 +20,29 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./Resize"; import { QueuesStatus } from "./QueueSatus"; import { OutputRepositoryStatus } from "./OutputRepositoryStatus"; import { GraphStoreStatus } from "./GraphStoreStatus"; -import { registerHuggingfaceLocalTasksInMemory } from "ellmers-ai-provider/hf-transformers/browser"; +import { InMemoryJobQueue } from "ellmers-storage/inmemory"; +import { registerHuggingfaceLocalTasks } from "ellmers-ai-provider/hf-transformers/browser"; +import { getProviderRegistry, ModelProcessorEnum } from "ellmers-ai"; +import { registerMediaPipeTfJsLocalTasks } from "ellmers-ai-provider/tf-mediapipe/browser"; import "ellmers-task"; +import "ellmers-test"; + +const ProviderRegistry = getProviderRegistry(); + +registerHuggingfaceLocalTasks(); +ProviderRegistry.registerQueue( + ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, + new InMemoryJobQueue("local_hft", new ConcurrencyLimiter(1, 10), 10) +); + +registerMediaPipeTfJsLocalTasks(); +ProviderRegistry.registerQueue( + ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, + new InMemoryJobQueue("local_mp", new ConcurrencyLimiter(1, 10), 10) +); -registerHuggingfaceLocalTasksInMemory(); +ProviderRegistry.clearQueues(); +ProviderRegistry.startQueues(); const taskOutputCache = new IndexedDbTaskOutputRepository(); const builder = new TaskGraphBuilder(taskOutputCache); diff --git a/examples/web/src/RunGraphFlow.tsx b/examples/web/src/RunGraphFlow.tsx index a87d9d6..cd20101 100644 --- a/examples/web/src/RunGraphFlow.tsx +++ b/examples/web/src/RunGraphFlow.tsx @@ -14,16 +14,11 @@ import { TurboNodeData, SingleNode, CompoundNode } from "./TurboNode"; import TurboEdge from "./TurboEdge"; import { FiFileText, FiClipboard, FiDownload, FiUpload } from "react-icons/fi"; import { Task, TaskGraph } from "ellmers-core"; -import { registerHuggingfaceLocalTasksInMemory } from "ellmers-ai-provider/hf-transformers/browser"; -import { registerMediaPipeTfJsLocalInMemory } from "ellmers-ai-provider/tf-mediapipe/browser"; import { GraphPipelineCenteredLayout, GraphPipelineLayout, computeLayout } from "./layout"; import "@xyflow/react/dist/base.css"; import "./RunGraphFlow.css"; -registerHuggingfaceLocalTasksInMemory(); -registerMediaPipeTfJsLocalInMemory(); - const categoryIcons = { "Text Model": , Input: , diff --git a/package.json b/package.json index 21ce6be..ed6f83d 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,12 @@ "build:ai-provider": "cd packages/ai-provider && bun run build", "build:storage": "cd packages/storage && bun run build", "build:task": "cd packages/task && bun run build", + "build:test": "cd packages/test && bun run build", "build:examples": "bun run bun run build:cli && bun run build:web", "build:cli": "cd examples/cli && bun run build", "build:web": "cd examples/web && bun run build", "clean": "rm -rf node_modules packages/*/node_modules packages/*/dist packages/*/src/**/*\\.d\\.ts packages/*/src/**/*\\.map examples/*/node_modules examples/*/dist examples/*/src/**/*\\.d\\.ts examples/*/src/**/*\\.map", - "watch:packages": "concurrently --kill-others -c 'auto' -n core,task,storage,ai,provider 'cd packages/core && bun run watch' 'sleep 3 && cd packages/task && bun run watch' 'sleep 3 && cd packages/storage && bun run watch' 'sleep 3 && cd packages/ai && bun run watch' 'sleep 6 && cd packages/ai-provider && bun run watch'", + "watch:packages": "concurrently --kill-others -c 'auto' -n core,task,storage,ai,provider,test 'cd packages/core && bun run watch' 'sleep 3 && cd packages/task && bun run watch' 'sleep 3 && cd packages/storage && bun run watch' 'sleep 3 && cd packages/ai && bun run watch' 'sleep 6 && cd packages/ai-provider && bun run watch' 'sleep 10 && cd packages/test && bun run watch'", "docs": "typedoc", "format": "eslint \"{packages|examples}/*/src/**/*.{js,ts,tsx,json}\" --fix && prettier \"{packages|examples}/*/src/**/*.{js,ts,tsx,json}\" --check --write", "release": "bun run build && bun publish", diff --git a/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts b/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts index 749cba2..3c1dded 100644 --- a/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts +++ b/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts @@ -6,32 +6,74 @@ // ******************************************************************************* import { describe, expect, it } from "bun:test"; -import { ConcurrencyLimiter, TaskInput, TaskOutput } from "ellmers-core"; -import { getProviderRegistry, ModelProcessorEnum } from "ellmers-ai"; +import { ConcurrencyLimiter, TaskGraphBuilder, TaskInput, TaskOutput } from "ellmers-core"; +import { getProviderRegistry, ModelProcessorEnum, ModelUseCaseEnum } from "ellmers-ai"; import { InMemoryJobQueue } from "ellmers-storage/inmemory"; +import { SqliteJobQueue } from "ellmers-storage/bun/sqlite"; import { registerHuggingfaceLocalTasks } from "../bindings/registerTasks"; -import "../model/ONNXModelSamples"; +import { getDatabase } from "../../../../storage/src/util/db_sqlite"; +import { sleep } from "bun"; +import { ONNXTransformerJsModel } from "../model/ONNXTransformerJsModel"; const HFQUEUE = "local_hf"; -export async function registerHuggingfaceLocalTasksInMemory() { - registerHuggingfaceLocalTasks(); - const providerRegistry = getProviderRegistry(); - const jobQueue = new InMemoryJobQueue( - HFQUEUE, - new ConcurrencyLimiter(1, 10), - 10 - ); - providerRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); - jobQueue.start(); - return providerRegistry; -} +describe("HFTransformersBinding", () => { + describe("InMemoryJobQueue", () => { + it("Should have an item queued", async () => { + // the model gets self-registered + const flanT5p786m = new ONNXTransformerJsModel( + "Xenova/LaMini-Flan-T5-783M", + [ModelUseCaseEnum.TEXT_GENERATION, ModelUseCaseEnum.TEXT_REWRITING], + "text2text-generation" + ); + registerHuggingfaceLocalTasks(); + const providerRegistry = getProviderRegistry(); + const jobQueue = new InMemoryJobQueue( + HFQUEUE, + new ConcurrencyLimiter(1, 10), + 10 + ); + providerRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); + const queue = providerRegistry.getQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS); + expect(queue).toBeDefined(); + expect(queue?.queue).toEqual(HFQUEUE); -describe("HFTransformersBinding.", () => { - it("should not fail", async () => { - const providerRegistry = await registerHuggingfaceLocalTasksInMemory(); - const queue = providerRegistry.getQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS); - expect(queue).toBeDefined(); - expect(queue?.queue).toEqual(HFQUEUE); + const builder = new TaskGraphBuilder(); + builder.DownloadModel({ + model: "Xenova/LaMini-Flan-T5-783M", + }); + builder.run(); + await sleep(1); + expect(await queue?.size()).toEqual(1); + await queue?.clear(); + }); + }); + + describe("SqliteJobQueue", () => { + it("Should have an item queued", async () => { + registerHuggingfaceLocalTasks(); + const providerRegistry = getProviderRegistry(); + const jobQueue = new SqliteJobQueue( + getDatabase(), + HFQUEUE, + new ConcurrencyLimiter(1, 10), + 10 + ); + jobQueue.ensureTableExists(); + providerRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); + const queue = providerRegistry.getQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS); + expect(queue).toBeDefined(); + expect(queue?.queue).toEqual(HFQUEUE); + + const builder = new TaskGraphBuilder(); + builder.DownloadModel({ + model: "Xenova/LaMini-Flan-T5-783M", + }); + builder.run(); + await sleep(1); + expect(await queue?.size()).toEqual(1); + builder.reset(); + await queue?.clear(); + }); }); }); diff --git a/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipe.test.ts b/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipe.test.ts deleted file mode 100644 index 9651f21..0000000 --- a/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipe.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -// ******************************************************************************* -// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * -// * * -// * Copyright Steven Roussey * -// * Licensed under the Apache License, Version 2.0 (the "License"); * -// ******************************************************************************* - -import { describe, expect, it } from "bun:test"; -import { ConcurrencyLimiter, TaskInput, TaskOutput } from "ellmers-core"; -import { getProviderRegistry, ModelProcessorEnum } from "ellmers-ai"; -import { InMemoryJobQueue } from "ellmers-storage/inmemory"; -import { registerMediaPipeTfJsLocalTasks } from "../bindings/registerTasks"; -import "../model/MediaPipeModelSamples"; - -const TFQUEUE = "local_tf-mediapipe"; - -export async function registerMediaPipeTfJsLocalInMemory() { - registerMediaPipeTfJsLocalTasks(); - const ProviderRegistry = getProviderRegistry(); - const jobQueue = new InMemoryJobQueue( - TFQUEUE, - new ConcurrencyLimiter(1, 10), - 10 - ); - ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); - jobQueue.start(); - return ProviderRegistry; -} - -describe("TfMediaPipe.", () => { - it("should not fail", async () => { - const providerRegistry = await registerMediaPipeTfJsLocalInMemory(); - const queue = providerRegistry.getQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL); - expect(queue).toBeDefined(); - expect(queue?.queue).toEqual(TFQUEUE); - }); -}); diff --git a/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts b/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts new file mode 100644 index 0000000..ee9ed8d --- /dev/null +++ b/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts @@ -0,0 +1,88 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { describe, expect, it } from "bun:test"; +import { ConcurrencyLimiter, TaskGraphBuilder, TaskInput, TaskOutput } from "ellmers-core"; +import { getProviderRegistry, ModelProcessorEnum, ModelUseCaseEnum } from "ellmers-ai"; +import { InMemoryJobQueue } from "ellmers-storage/inmemory"; +import { SqliteJobQueue } from "ellmers-storage/bun/sqlite"; +import { registerMediaPipeTfJsLocalTasks } from "../bindings/registerTasks"; +import { sleep } from "ellmers-core"; +import { MediaPipeTfJsModel } from "../model/MediaPipeModel"; +import { getDatabase } from "../../../../storage/src/util/db_sqlite"; + +const TFQUEUE = "local_tf-mediapipe"; + +describe("TfMediaPipeBinding", () => { + describe("InMemoryJobQueue", () => { + it("should not fail", async () => { + // register on creation + const universal_sentence_encoder = new MediaPipeTfJsModel( + "Universal Sentence Encoder", + [ModelUseCaseEnum.TEXT_EMBEDDING], + "https://storage.googleapis.com/mediapipe-tasks/text_embedder/universal_sentence_encoder.tflite", + { dimensions: 100, browserOnly: true } + ); + registerMediaPipeTfJsLocalTasks(); + const ProviderRegistry = getProviderRegistry(); + const jobQueue = new InMemoryJobQueue( + TFQUEUE, + new ConcurrencyLimiter(1, 10), + 10 + ); + ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); + const queue = ProviderRegistry.getQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL); + expect(queue).toBeDefined(); + expect(queue?.queue).toEqual(TFQUEUE); + + const builder = new TaskGraphBuilder(); + builder.DownloadModel({ + model: "Universal Sentence Encoder", + }); + builder.run(); + await sleep(1); + // we are not in a browser context, so the model should not be registered + expect(await queue?.size()).toEqual(0); + builder.reset(); + await queue?.clear(); + }); + }); + describe("SqliteJobQueue", () => { + it("should not fail", async () => { + const universal_sentence_encoder = new MediaPipeTfJsModel( + "Universal Sentence Encoder", + [ModelUseCaseEnum.TEXT_EMBEDDING], + "https://storage.googleapis.com/mediapipe-tasks/text_embedder/universal_sentence_encoder.tflite", + { dimensions: 100, browserOnly: true } + ); + registerMediaPipeTfJsLocalTasks(); + const ProviderRegistry = getProviderRegistry(); + const jobQueue = new SqliteJobQueue( + getDatabase(":memory:"), + TFQUEUE, + new ConcurrencyLimiter(1, 10), + 10 + ); + jobQueue.ensureTableExists(); + ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); + const queue = ProviderRegistry.getQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL); + expect(queue).toBeDefined(); + expect(queue?.queue).toEqual(TFQUEUE); + + const builder = new TaskGraphBuilder(); + builder.DownloadModel({ + model: "Universal Sentence Encoder", + }); + builder.run(); + await sleep(1); + // we are not in a browser context, so the model should not be registered + expect(await queue?.size()).toEqual(0); + builder.reset(); + await queue?.clear(); + }); + }); +}); diff --git a/packages/core/src/job/base/JobQueue.ts b/packages/core/src/job/base/JobQueue.ts index 930b78c..62949b2 100644 --- a/packages/core/src/job/base/JobQueue.ts +++ b/packages/core/src/job/base/JobQueue.ts @@ -124,11 +124,13 @@ export abstract class JobQueue { this.running = true; this.events.emit("queue_start", this.queue); this.processJobs(); + return this; } async stop() { this.running = false; this.events.emit("queue_stop", this.queue); + return this; } async restart() { @@ -138,5 +140,6 @@ export abstract class JobQueue { this.waits.forEach(({ reject }) => reject("Queue Restarted")); this.waits.clear(); await this.start(); + return this; } } diff --git a/packages/storage/src/browser/inmemory/InMemoryJobQueue.ts b/packages/storage/src/browser/inmemory/InMemoryJobQueue.ts index 9aef9e2..bc5e350 100644 --- a/packages/storage/src/browser/inmemory/InMemoryJobQueue.ts +++ b/packages/storage/src/browser/inmemory/InMemoryJobQueue.ts @@ -10,7 +10,12 @@ import { Job, JobStatus, JobQueue, ILimiter } from "ellmers-core"; import { makeFingerprint } from "../../util/Misc"; export class InMemoryJobQueue extends JobQueue { - constructor(queue: string, limiter: ILimiter, waitDurationInMilliseconds = 100) { + constructor( + queue: string, + limiter: ILimiter, + waitDurationInMilliseconds = 100, + protected jobClass: typeof Job = Job + ) { super(queue, limiter, waitDurationInMilliseconds); this.jobQueue = []; } diff --git a/packages/storage/src/bun/sqlite/SqliteJobQueue.ts b/packages/storage/src/bun/sqlite/SqliteJobQueue.ts index dec6b6f..6df5ed5 100644 --- a/packages/storage/src/bun/sqlite/SqliteJobQueue.ts +++ b/packages/storage/src/bun/sqlite/SqliteJobQueue.ts @@ -16,14 +16,14 @@ export class SqliteJobQueue extends JobQueue { protected db: Database, queue: string, limiter: ILimiter, - protected jobClass: typeof Job = Job, - waitDurationInMilliseconds = 100 + waitDurationInMilliseconds = 100, + protected jobClass: typeof Job = Job ) { super(queue, limiter, waitDurationInMilliseconds); } public ensureTableExists() { - this.db.exec(` + const a = this.db.exec(` CREATE TABLE IF NOT EXISTS job_queue ( id INTEGER PRIMARY KEY, diff --git a/packages/storage/src/bun/sqlite/test/SqliteJobQueue.test.ts b/packages/storage/src/bun/sqlite/test/SqliteJobQueue.test.ts index c5730e8..48169bc 100644 --- a/packages/storage/src/bun/sqlite/test/SqliteJobQueue.test.ts +++ b/packages/storage/src/bun/sqlite/test/SqliteJobQueue.test.ts @@ -26,8 +26,8 @@ describe("SqliteJobQueue", () => { db, queueName, new SqliteRateLimiter(db, queueName, 4, 1).ensureTableExists(), - TestJob, - 0 + 0, + TestJob ).ensureTableExists(); afterEach(() => { diff --git a/packages/test/package.json b/packages/test/package.json new file mode 100644 index 0000000..9cf2779 --- /dev/null +++ b/packages/test/package.json @@ -0,0 +1,32 @@ +{ + "name": "ellmers-test", + "type": "module", + "version": "0.0.1", + "description": "Ellmers is a tool for building and running DAG pipelines of AI tasks.", + "scripts": { + "watch": "concurrently -c 'auto' 'bun:watch-*'", + "watch-code": "bun build --watch --no-clear-screen --target=browser --sourcemap=external --external @mediapipe/tasks-text --external @huggingface/transformers --external ellmers-core --external ellmers-ai --external ellmers-ai-provider --external ellmers-storage --outdir ./dist/ ./src/index.ts", + "watch-types": "tsc --watch --preserveWatchOutput", + "build": "bun run build-clean && bun run build-types && bun run build-hf-transformers && bun run build-tf-mediapipe", + "build-clean": "rm -fr dist/* tsconfig.tsbuildinfo", + "build-code": "bun build --target=browser --sourcemap=external --external @mediapipe/tasks-text --external @mediapipe/tasks-text --external @huggingface/transformers --external ellmers-core --external ellmers-ai --external ellmers-ai-provider --external ellmers-storage --outdir ./dist/ ./src/index.ts", + "build-types": "tsc", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "test": "bun test" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "dependencies": { + "ellmers-core": "workspace:packages/core", + "ellmers-ai": "workspace:packages/ai", + "ellmers-ai-provider": "workspace:packages/ai-provider", + "ellmers-storage": "workspace:packages/storage" + } +} diff --git a/packages/test/src/index.ts b/packages/test/src/index.ts new file mode 100644 index 0000000..a0a4d19 --- /dev/null +++ b/packages/test/src/index.ts @@ -0,0 +1,32 @@ +import { getProviderRegistry, ModelProcessorEnum } from "ellmers-ai"; +import { registerHuggingfaceLocalTasks } from "ellmers-ai-provider/hf-transformers/browser"; +import { registerMediaPipeTfJsLocalTasks } from "ellmers-ai-provider/tf-mediapipe/browser"; +import { ConcurrencyLimiter, TaskInput, TaskOutput } from "ellmers-core"; +import { InMemoryJobQueue } from "ellmers-storage/inmemory"; + +export * from "./sample/MediaPipeModelSamples"; +export * from "./sample/ONNXModelSamples"; + +export async function registerHuggingfaceLocalTasksInMemory() { + registerHuggingfaceLocalTasks(); + const ProviderRegistry = getProviderRegistry(); + const jobQueue = new InMemoryJobQueue( + "local_hf", + new ConcurrencyLimiter(1, 10), + 10 + ); + ProviderRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); + jobQueue.start(); +} + +export async function registerMediaPipeTfJsLocalInMemory() { + registerMediaPipeTfJsLocalTasks(); + const ProviderRegistry = getProviderRegistry(); + const jobQueue = new InMemoryJobQueue( + "local_media_pipe", + new ConcurrencyLimiter(1, 10), + 10 + ); + ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); + jobQueue.start(); +} diff --git a/packages/ai-provider/src/tf-mediapipe/model/MediaPipeModelSamples.ts b/packages/test/src/sample/MediaPipeModelSamples.ts similarity index 86% rename from packages/ai-provider/src/tf-mediapipe/model/MediaPipeModelSamples.ts rename to packages/test/src/sample/MediaPipeModelSamples.ts index 3e6e30a..6c04223 100644 --- a/packages/ai-provider/src/tf-mediapipe/model/MediaPipeModelSamples.ts +++ b/packages/test/src/sample/MediaPipeModelSamples.ts @@ -1,5 +1,5 @@ import { ModelUseCaseEnum } from "ellmers-ai"; -import { MediaPipeTfJsModel } from "./MediaPipeModel"; +import { MediaPipeTfJsModel } from "../../../ai-provider/src/tf-mediapipe/model/MediaPipeModel"; export const universal_sentence_encoder = new MediaPipeTfJsModel( "Universal Sentence Encoder", diff --git a/packages/ai-provider/src/hf-transformers/model/ONNXModelSamples.ts b/packages/test/src/sample/ONNXModelSamples.ts similarity index 92% rename from packages/ai-provider/src/hf-transformers/model/ONNXModelSamples.ts rename to packages/test/src/sample/ONNXModelSamples.ts index f57ccf0..3adc5b5 100644 --- a/packages/ai-provider/src/hf-transformers/model/ONNXModelSamples.ts +++ b/packages/test/src/sample/ONNXModelSamples.ts @@ -1,4 +1,4 @@ -import { DATA_TYPES, ONNXTransformerJsModel } from "./ONNXTransformerJsModel"; +import { DATA_TYPES, ONNXTransformerJsModel } from "ellmers-ai-provider/hf-transformers/browser"; import { ModelUseCaseEnum } from "ellmers-ai"; export const supabaseGteSmall = new ONNXTransformerJsModel( @@ -48,6 +48,12 @@ export const xenovaDistilbertMnli = new ONNXTransformerJsModel( "zero-shot-classification" ); +export const modernBertBase = new ONNXTransformerJsModel( + "answerdotai/ModernBERT-base", + [ModelUseCaseEnum.TEXT_CLASSIFICATION], + "fill-mask" +); + export const stentancetransformerMultiQaMpnetBaseDotV1 = new ONNXTransformerJsModel( "Xenova/multi-qa-mpnet-base-dot-v1", [ModelUseCaseEnum.TEXT_EMBEDDING], diff --git a/packages/test/src/util/db_sqlite.ts b/packages/test/src/util/db_sqlite.ts new file mode 100644 index 0000000..fa89038 --- /dev/null +++ b/packages/test/src/util/db_sqlite.ts @@ -0,0 +1,21 @@ +const wrapper = function () { + if (process["isBun"]) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require("bun:sqlite").Database; + } + + return require("better-sqlite3"); +}; + +const module = wrapper(); + +let db: any; + +export function getDatabase(name = ":memory:"): any { + if (!db) { + db = new module(name); + } + return db; +} + +export default module; diff --git a/packages/test/tsconfig.json b/packages/test/tsconfig.json new file mode 100644 index 0000000..ad55c7f --- /dev/null +++ b/packages/test/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*"], + "files": ["src/index.ts"], + "exclude": ["**/*.test.ts", "dist"], + "compilerOptions": { + "outDir": "./dist", + "baseUrl": "./src", + "rootDir": "./src", + "paths": { + "#/*": ["./src/*"], + "ellmers-core": ["../core/src"], + "ellmers-ai": ["../ai/src"], + "ellmers-ai-provider": ["../ai-provider/src"], + "ellmers-storage": ["../storage/src"] + } + }, + "references": [ + { "path": "../core" }, + { "path": "../ai" }, + { "path": "../ai-provider" }, + { "path": "../storage" } + ] +} From 01593dffcde7239868ca4a4bc84a59f9d1e55a08 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Wed, 15 Jan 2025 21:21:00 -0800 Subject: [PATCH 03/12] refactor: big changeset to KVRepositories --- .../core/src/storage/base/KVRepository.ts | 134 +++++++++---- .../storage/taskgraph/TaskGraphRepository.ts | 21 ++- .../taskoutput/TaskOutputRepository.ts | 27 ++- .../IndexedDbTaskOutputRepository.ts | 11 +- .../indexeddb/base/IndexedDbKVRepository.ts | 50 +++-- .../browser/inmemory/InMemoryKVRepository.ts | 42 ----- .../inmemory/InMemoryTaskGraphRepository.ts | 8 +- .../inmemory/InMemoryTaskOutputRepository.ts | 25 ++- .../inmemory/base/InMemoryKVRepository.ts | 66 +++++++ .../storage/src/browser/inmemory/index.ts | 2 +- .../test/InMemoryKVRepository.test.ts | 73 ++++++++ .../test/InMemoryTaskGraphRepository.test.ts | 2 - .../bun/sqlite/SqliteTaskGraphRepository.ts | 6 +- .../bun/sqlite/SqliteTaskOutputRepository.ts | 21 ++- .../src/bun/sqlite/base/SqliteKVRepository.ts | 176 +++++++++++++----- .../sqlite/test/SqliteKVRepository.test.ts | 78 ++++++++ .../filesystem/FileTaskGraphRepository.ts | 6 +- .../filesystem/FileTaskOutputRepository.ts | 23 ++- .../node/filesystem/base/FileKVRepository.ts | 93 +++++---- .../filesystem/test/FileKVRepository.test.ts | 94 ++++++++++ .../postgres/PostgresTaskGraphRepository.ts | 9 +- .../postgres/PostgresTaskOutputRepository.ts | 21 ++- .../postgres/base/PostgresKVRepository.ts | 160 +++++++++++----- 23 files changed, 864 insertions(+), 284 deletions(-) delete mode 100644 packages/storage/src/browser/inmemory/InMemoryKVRepository.ts create mode 100644 packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts create mode 100644 packages/storage/src/browser/inmemory/test/InMemoryKVRepository.test.ts create mode 100644 packages/storage/src/bun/sqlite/test/SqliteKVRepository.test.ts create mode 100644 packages/storage/src/node/filesystem/test/FileKVRepository.test.ts diff --git a/packages/core/src/storage/base/KVRepository.ts b/packages/core/src/storage/base/KVRepository.ts index 3bc1ea6..e373251 100644 --- a/packages/core/src/storage/base/KVRepository.ts +++ b/packages/core/src/storage/base/KVRepository.ts @@ -6,15 +6,27 @@ // ******************************************************************************* import EventEmitter from "eventemitter3"; +import { makeFingerprint } from "../../util/Misc"; -export type KVEvents = "put" | "get" | "clear"; +export type KVEvents = "put" | "get" | "delete" | "clearall"; +export type BasicKeyType = string | number | bigint; +export type BasicValueType = string | number | bigint | boolean | null; -export type DiscriminatorSchema = Record; +export type BasePrimaryKeySchema = Record; +export type BaseValueSchema = Record; + +export type DefaultPrimaryKeyType = { "kv-key": string }; +export const DefaultPrimaryKeySchema: BasePrimaryKeySchema = { "kv-key": "string" } as const; + +export type DefaultValueType = { "kv-value": string }; +export const DefaultValueSchema: BaseValueSchema = { "kv-value": "string" } as const; export abstract class KVRepository< - Key, - Value, - Discriminators extends DiscriminatorSchema = DiscriminatorSchema, + Key extends Record = DefaultPrimaryKeyType, + Value extends Record = DefaultValueType, + PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, + ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, + Combined extends Key & Value = Key & Value > { // KV repository event emitter private events = new EventEmitter(); @@ -37,45 +49,99 @@ export abstract class KVRepository< this.events.emit.call(this.events, name, ...args); } - // discriminators for KV repository store - protected discriminatorsSchema: Discriminators = {} as Discriminators; + protected primaryKeyIndex: string | undefined = undefined; + protected valueIndex: string | undefined = undefined; + constructor( + protected primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, + protected valueSchema: ValueSchema = DefaultValueSchema as ValueSchema + ) { + this.primaryKeySchema = primaryKeySchema; + this.valueSchema = valueSchema; + if (Object.keys(primaryKeySchema).length === 1) { + this.primaryKeyIndex = String(this.primaryKeyColumns()[0]); + } + if (Object.keys(valueSchema).length === 1) { + this.valueIndex = String(this.valueColumns()[0]); + } + } // Abstract methods for KV repository store - abstract put(key: Key, value: Value): Promise; - abstract get(key: Key): Promise; - abstract clear(): Promise; + abstract putKeyValue(key: Key, value: Value): Promise; + abstract getKeyValue(key: Key): Promise; + abstract deleteKeyValue(key: Key | Combined): Promise; + abstract deleteAll(): Promise; abstract size(): Promise; - // Discriminator helper methods - protected primaryKeyColumnList(): string { - return this.primaryKeyColumns().join(", "); + public put(key: BasicKeyType | Key, value: Value | BasicValueType): Promise { + if (typeof key !== "object" && this.primaryKeyIndex) { + key = { [this.primaryKeyIndex]: key } as Key; + if (typeof value !== "object" && this.valueIndex) { + value = { [this.valueIndex]: value } as Value; + } + } + return this.putKeyValue(key as Key, value as Value); + } + + public async get(key: BasicKeyType | Key): Promise { + if (typeof key !== "object" && this.primaryKeyIndex) { + key = { [this.primaryKeyIndex]: key } as Key; + } + const value = await this.getKeyValue(key as Key); + if (typeof value !== "object") return value; + if (this.primaryKeyIndex && this.valueIndex) { + return value[this.valueIndex] as BasicValueType; + } + return value as Value; } - protected primaryKeyColumns(): string[] { - return Object.keys(this.discriminatorsSchema).concat("key"); + public async getCombined(key: Key): Promise { + const value = await this.getKeyValue(key); + if (typeof value !== "object") return undefined; + return Object.assign({}, key, value) as Combined; } - protected extractDiscriminators(keySimpleOrObject: any): { - discriminators: Record; - key: any; - } { - const discriminatorKeys = Object.keys(this.discriminatorsSchema); - const discriminators: DiscriminatorSchema = {}; - if (typeof keySimpleOrObject !== "object") { - return { discriminators, key: keySimpleOrObject }; + public delete(key: Key | BasicKeyType): Promise { + if (typeof key !== "object" && this.primaryKeyIndex) { + key = { [this.primaryKeyIndex]: key } as Key; } - let keyClone: any = { ...keySimpleOrObject }; - if (discriminatorKeys.length > 0) { - discriminatorKeys.forEach((k) => { - if (Object.prototype.hasOwnProperty.call(keyClone, k)) { - discriminators[k] = keyClone[k]; - delete keyClone[k]; - } - }); + return this.deleteKeyValue(key as Key); + } + + protected primaryKeyColumns(): Array { + return Object.keys(this.primaryKeySchema); + } + + protected valueColumns(): Array { + return Object.keys(this.valueSchema); + } + + protected separateKeyValueFromCombined(obj: Combined): { value: Value; key: Key } { + if (obj === null) { + console.warn("Key is null"); + return { value: {} as Value, key: {} as Key }; + } + if (typeof obj !== "object") { + console.warn("Object is not an object"); + return { value: {} as Value, key: {} as Key }; } - if (Object.keys(keyClone).length === 1) { - keyClone = keyClone[Object.keys(keyClone)[0]]; + const primaryKeyNames = this.primaryKeyColumns(); + const valueNames = this.valueColumns(); + const value: Partial = {}; + const key: Partial = {}; + for (const k of primaryKeyNames) { + key[k] = obj[k]; + } + for (const k of valueNames) { + value[k] = obj[k]; + } + + return { value: value as Value, key: key as Key }; + } + + protected async getKeyAsIdString(key: Key | BasicKeyType): Promise { + if (this.primaryKeyIndex && typeof key === "object") { + key = key[this.primaryKeyIndex]; } - return { discriminators, key: keyClone }; + return await makeFingerprint(key); } } diff --git a/packages/core/src/storage/taskgraph/TaskGraphRepository.ts b/packages/core/src/storage/taskgraph/TaskGraphRepository.ts index bcf7b6c..8fd5491 100644 --- a/packages/core/src/storage/taskgraph/TaskGraphRepository.ts +++ b/packages/core/src/storage/taskgraph/TaskGraphRepository.ts @@ -15,7 +15,7 @@ export type TaskGraphEvents = "graph_saved" | "graph_retrieved" | "graph_cleared export abstract class TaskGraphRepository { public type = "TaskGraphRepository"; - abstract kvRepository: KVRepository; + abstract kvRepository: KVRepository; private events = new EventEmitter(); on(name: TaskGraphEvents, fn: (...args: any[]) => void) { this.events.on.call(this.events, name, fn); @@ -69,26 +69,27 @@ export abstract class TaskGraphRepository { return subGraph; } - async saveTaskGraph(id: unknown, output: TaskGraph): Promise { - const jsonObj = output.toJSON(); - await this.kvRepository.put(id, jsonObj); - this.emit("graph_saved", id); + async saveTaskGraph(key: string, output: TaskGraph): Promise { + const value = JSON.stringify(output.toJSON()); + await this.kvRepository.put(key, value); + this.emit("graph_saved", key); } - async getTaskGraph(id: unknown): Promise { - const jsonObj = await this.kvRepository.get(id); - if (!jsonObj) { + async getTaskGraph(key: string): Promise { + const jsonStr = (await this.kvRepository.get(key)) as string; + if (!jsonStr) { return undefined; } + const jsonObj = JSON.parse(jsonStr); const graph = this.createSubGraph(jsonObj); - this.emit("graph_retrieved", id); + this.emit("graph_retrieved", key); return graph; } async clear(): Promise { - await this.kvRepository.clear(); + await this.kvRepository.deleteAll(); this.emit("graph_cleared"); } diff --git a/packages/core/src/storage/taskoutput/TaskOutputRepository.ts b/packages/core/src/storage/taskoutput/TaskOutputRepository.ts index a8a8686..97a087b 100644 --- a/packages/core/src/storage/taskoutput/TaskOutputRepository.ts +++ b/packages/core/src/storage/taskoutput/TaskOutputRepository.ts @@ -7,17 +7,27 @@ import EventEmitter from "eventemitter3"; import { TaskInput, TaskOutput } from "../../task/base/Task"; -import { KVRepository } from "../base/KVRepository"; +import { DefaultValueType, KVRepository } from "../base/KVRepository"; +import { makeFingerprint } from "../../util/Misc"; export type TaskOutputEvents = "output_saved" | "output_retrieved" | "output_cleared"; -export const TaskOutputDiscriminator = { +export type TaskOutputPrimaryKey = { + key: string; + taskType: string; +}; +export const TaskOutputPrimaryKeySchema = { + key: "string", taskType: "string", } as const; export abstract class TaskOutputRepository { public type = "TaskOutputRepository"; - abstract kvRepository: KVRepository; + abstract kvRepository: KVRepository< + TaskOutputPrimaryKey, + DefaultValueType, + typeof TaskOutputPrimaryKeySchema + >; private events = new EventEmitter(); on(name: TaskOutputEvents, fn: (...args: any[]) => void) { this.events.on.call(this.events, name, fn); @@ -30,18 +40,21 @@ export abstract class TaskOutputRepository { } async saveOutput(taskType: string, inputs: TaskInput, output: TaskOutput): Promise { - await this.kvRepository.put({ taskType, inputs }, output); + const key = await makeFingerprint(inputs); + const value = JSON.stringify(output); + await this.kvRepository.putKeyValue({ key, taskType }, { "kv-value": value }); this.emit("output_saved", taskType); } async getOutput(taskType: string, inputs: TaskInput): Promise { - const output = await this.kvRepository.get({ taskType, inputs }); + const key = await makeFingerprint(inputs); + const output = await this.kvRepository.getKeyValue({ key, taskType }); this.emit("output_retrieved", taskType); - return output as TaskOutput; + return output ? (JSON.parse(output["kv-value"]) as TaskOutput) : undefined; } async clear(): Promise { - await this.kvRepository.clear(); + await this.kvRepository.deleteAll(); this.emit("output_cleared"); } diff --git a/packages/storage/src/browser/indexeddb/IndexedDbTaskOutputRepository.ts b/packages/storage/src/browser/indexeddb/IndexedDbTaskOutputRepository.ts index 0f83e0d..f3b7b27 100644 --- a/packages/storage/src/browser/indexeddb/IndexedDbTaskOutputRepository.ts +++ b/packages/storage/src/browser/indexeddb/IndexedDbTaskOutputRepository.ts @@ -5,18 +5,23 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { TaskInput, TaskOutput, TaskOutputDiscriminator, TaskOutputRepository } from "ellmers-core"; +import { + TaskInput, + TaskOutput, + TaskOutputPrimaryKeySchema, + TaskOutputRepository, +} from "ellmers-core"; import { IndexedDbKVRepository } from "./base/IndexedDbKVRepository"; export class IndexedDbTaskOutputRepository extends TaskOutputRepository { - kvRepository: IndexedDbKVRepository; + kvRepository: IndexedDbKVRepository; public type = "IndexedDbTaskOutputRepository" as const; constructor() { super(); this.kvRepository = new IndexedDbKVRepository< TaskInput, TaskOutput, - typeof TaskOutputDiscriminator + typeof TaskOutputPrimaryKeySchema >("task_outputs"); } } diff --git a/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts b/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts index 0895590..defcd84 100644 --- a/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts +++ b/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts @@ -5,7 +5,16 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { DiscriminatorSchema, KVRepository } from "ellmers-core"; +import { + BaseValueSchema, + BasePrimaryKeySchema, + BasicKeyType, + DefaultValueType, + DefaultValueSchema, + DefaultPrimaryKeyType, + DefaultPrimaryKeySchema, + KVRepository, +} from "ellmers-core"; import { ensureIndexedDbTable } from "./IndexedDbTable"; import { makeFingerprint } from "../../../util/Misc"; @@ -13,10 +22,12 @@ import { makeFingerprint } from "../../../util/Misc"; // simple browser-based examples with no server-side component. It does not support di export class IndexedDbKVRepository< - Key = string, - Value = string, - Discriminator extends DiscriminatorSchema = DiscriminatorSchema -> extends KVRepository { + Key extends Record = DefaultPrimaryKeyType, + Value extends Record = DefaultValueType, + PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, + ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, + Combined extends Key & Value = Key & Value +> extends KVRepository { private dbPromise: Promise; constructor(public table: string = "kv_store") { @@ -26,8 +37,8 @@ export class IndexedDbKVRepository< }); } - async put(key: Key, value: Value): Promise { - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); + async putKeyValue(key: Key, value: Value): Promise { + const id = await makeFingerprint(key); const db = await this.dbPromise; return new Promise((resolve, reject) => { @@ -43,8 +54,8 @@ export class IndexedDbKVRepository< }); } - async get(key: Key): Promise { - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); + async getKeyValue(key: Key): Promise { + const id = await makeFingerprint(key); const db = await this.dbPromise; return new Promise((resolve, reject) => { @@ -64,7 +75,24 @@ export class IndexedDbKVRepository< }); } - async clear(): Promise { + async deleteKeyValue(key: Key): Promise { + const id = await makeFingerprint(key); + const db = await this.dbPromise; + + return new Promise((resolve, reject) => { + const transaction = db.transaction(this.table, "readwrite"); + const store = transaction.objectStore(this.table); + const request = store.delete(id); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + this.emit("delete", id); + resolve(); + }; + }); + } + + async deleteAll(): Promise { const db = await this.dbPromise; return new Promise((resolve, reject) => { @@ -74,7 +102,7 @@ export class IndexedDbKVRepository< request.onerror = () => reject(request.error); request.onsuccess = () => { - this.emit("clear"); + this.emit("clearall"); resolve(); }; }); diff --git a/packages/storage/src/browser/inmemory/InMemoryKVRepository.ts b/packages/storage/src/browser/inmemory/InMemoryKVRepository.ts deleted file mode 100644 index 53078e6..0000000 --- a/packages/storage/src/browser/inmemory/InMemoryKVRepository.ts +++ /dev/null @@ -1,42 +0,0 @@ -// ******************************************************************************* -// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * -// * * -// * Copyright Steven Roussey * -// * Licensed under the Apache License, Version 2.0 (the "License"); * -// ******************************************************************************* - -import { DiscriminatorSchema, KVRepository } from "ellmers-core"; -import { makeFingerprint } from "../../util/Misc"; - -// InMemoryKVRepository is a simple in-memory key-value store that can be used for testing or as a cache -// It does not support discriminators - -export class InMemoryKVRepository< - Key = string, - Value = string, - Discriminator extends DiscriminatorSchema = DiscriminatorSchema -> extends KVRepository { - values = new Map(); - - async put(key: Key, value: Value): Promise { - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); - this.values.set(id, value); - this.emit("put", id); - } - - async get(key: Key): Promise { - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); - const out = this.values.get(id); - this.emit("get", id); - return out; - } - - async clear(): Promise { - this.values.clear(); - this.emit("clear"); - } - - async size(): Promise { - return this.values.size; - } -} diff --git a/packages/storage/src/browser/inmemory/InMemoryTaskGraphRepository.ts b/packages/storage/src/browser/inmemory/InMemoryTaskGraphRepository.ts index 92b7da9..84eb836 100644 --- a/packages/storage/src/browser/inmemory/InMemoryTaskGraphRepository.ts +++ b/packages/storage/src/browser/inmemory/InMemoryTaskGraphRepository.ts @@ -5,14 +5,14 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { TaskGraphJson, TaskGraphRepository } from "ellmers-core"; -import { InMemoryKVRepository } from "./InMemoryKVRepository"; +import { TaskGraphRepository } from "ellmers-core"; +import { InMemoryKVRepository } from "./base/InMemoryKVRepository"; export class InMemoryTaskGraphRepository extends TaskGraphRepository { - kvRepository: InMemoryKVRepository; + kvRepository: InMemoryKVRepository; public type = "InMemoryTaskGraphRepository" as const; constructor() { super(); - this.kvRepository = new InMemoryKVRepository(); + this.kvRepository = new InMemoryKVRepository(); } } diff --git a/packages/storage/src/browser/inmemory/InMemoryTaskOutputRepository.ts b/packages/storage/src/browser/inmemory/InMemoryTaskOutputRepository.ts index 9f030f7..66b294a 100644 --- a/packages/storage/src/browser/inmemory/InMemoryTaskOutputRepository.ts +++ b/packages/storage/src/browser/inmemory/InMemoryTaskOutputRepository.ts @@ -5,18 +5,29 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { TaskInput, TaskOutput, TaskOutputDiscriminator, TaskOutputRepository } from "ellmers-core"; -import { InMemoryKVRepository } from "./InMemoryKVRepository"; +import { + DefaultValueType, + TaskInput, + TaskOutput, + TaskOutputPrimaryKey, + TaskOutputPrimaryKeySchema, + TaskOutputRepository, +} from "ellmers-core"; +import { InMemoryKVRepository } from "./base/InMemoryKVRepository"; export class InMemoryTaskOutputRepository extends TaskOutputRepository { - kvRepository: InMemoryKVRepository; + kvRepository: InMemoryKVRepository< + TaskOutputPrimaryKey, + DefaultValueType, + typeof TaskOutputPrimaryKeySchema + >; public type = "InMemoryTaskOutputRepository" as const; constructor() { super(); this.kvRepository = new InMemoryKVRepository< - TaskInput, - TaskOutput, - typeof TaskOutputDiscriminator - >(); + TaskOutputPrimaryKey, + DefaultValueType, + typeof TaskOutputPrimaryKeySchema + >(TaskOutputPrimaryKeySchema); } } diff --git a/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts b/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts new file mode 100644 index 0000000..cb8f7c4 --- /dev/null +++ b/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts @@ -0,0 +1,66 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { + BaseValueSchema, + BasePrimaryKeySchema, + BasicValueType, + BasicKeyType, + DefaultValueType, + DefaultValueSchema, + DefaultPrimaryKeyType, + DefaultPrimaryKeySchema, + KVRepository, +} from "ellmers-core"; +import { makeFingerprint } from "../../../util/Misc"; + +// InMemoryKVRepository is a simple in-memory key-value store that can be used for testing or as a cache + +export class InMemoryKVRepository< + Key extends Record = DefaultPrimaryKeyType, + Value extends Record = DefaultValueType, + PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, + ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, + Combined extends Key & Value = Key & Value +> extends KVRepository { + values = new Map(); + + constructor( + primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, + valueSchema: ValueSchema = DefaultValueSchema as ValueSchema + ) { + super(primaryKeySchema, valueSchema); + } + + async putKeyValue(key: Key, value: Value): Promise { + const id = await makeFingerprint(key); + this.values.set(id, value); + this.emit("put", id); + } + + async getKeyValue(key: Key): Promise { + const id = await makeFingerprint(key); + const out = this.values.get(id); + this.emit("get", id, out); + return out; + } + + async deleteKeyValue(key: Key): Promise { + const id = await makeFingerprint(key); + this.values.delete(id); + this.emit("delete", id); + } + + async deleteAll(): Promise { + this.values.clear(); + this.emit("clearall"); + } + + async size(): Promise { + return this.values.size; + } +} diff --git a/packages/storage/src/browser/inmemory/index.ts b/packages/storage/src/browser/inmemory/index.ts index 5e9a4fe..fe1fefc 100644 --- a/packages/storage/src/browser/inmemory/index.ts +++ b/packages/storage/src/browser/inmemory/index.ts @@ -1,4 +1,4 @@ -export * from "./InMemoryKVRepository"; +export * from "./base/InMemoryKVRepository"; export * from "./InMemoryTaskOutputRepository"; export * from "./InMemoryTaskGraphRepository"; export * from "./InMemoryJobQueue"; diff --git a/packages/storage/src/browser/inmemory/test/InMemoryKVRepository.test.ts b/packages/storage/src/browser/inmemory/test/InMemoryKVRepository.test.ts new file mode 100644 index 0000000..a9c6502 --- /dev/null +++ b/packages/storage/src/browser/inmemory/test/InMemoryKVRepository.test.ts @@ -0,0 +1,73 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { describe, expect, it, beforeEach } from "bun:test"; +import { InMemoryKVRepository } from "../base/InMemoryKVRepository"; +import { BaseValueSchema, BasePrimaryKeySchema } from "ellmers-core"; + +type PrimaryKey = { + name: string; + type: string; +}; +type Value = { + option: string; + success: boolean; +}; + +export const PrimaryKeySchema: BasePrimaryKeySchema = { name: "string", type: "string" } as const; +export const ValueSchema: BaseValueSchema = { option: "string", success: "boolean" } as const; + +describe("InMemoryKVRepository", () => { + describe("with default schemas (key and value)", () => { + let repository: InMemoryKVRepository; + + beforeEach(() => { + repository = new InMemoryKVRepository(); + }); + + it("should store and retrieve values for a key", async () => { + const key = "key"; + const value = "value"; + await repository.put(key, value); + const output = await repository.get(key); + + expect(output).toEqual(value); + }); + it("should get undefined for a key that doesn't exist", async () => { + const key = "key"; + const value = "value"; + await repository.put(key, value); + const output = await repository.get("not-a-key"); + + expect(output == undefined).toEqual(true); + }); + }); + + describe("with complex schemas", () => { + let repository: InMemoryKVRepository; + + beforeEach(() => { + repository = new InMemoryKVRepository(PrimaryKeySchema, ValueSchema); + }); + + it("should store and retrieve values for a key", async () => { + const key = { name: "key", type: "string" }; + const value = { option: "value", success: true }; + await repository.put(key, value); + const output = await repository.getKeyValue(key); + + expect(output?.option).toEqual("value"); + expect(!!output?.success).toEqual(true); // TODO need some conversion to boolean from 1 + }); + it("should get undefined for a key that doesn't exist", async () => { + const key = { name: "key", type: "string" }; + const output = await repository.get(key); + + expect(output == undefined).toEqual(true); + }); + }); +}); diff --git a/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts b/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts index c18b483..96c798d 100644 --- a/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts +++ b/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts @@ -6,7 +6,6 @@ // ******************************************************************************* import { describe, expect, it, beforeEach } from "bun:test"; -import { rmdirSync } from "fs"; import { SingleTask, TaskOutput, DataFlow, TaskGraph, TaskRegistry } from "ellmers-core"; import { InMemoryTaskGraphRepository } from "ellmers-storage/inmemory"; @@ -22,7 +21,6 @@ describe("FileTaskGraphRepository", () => { let repository: InMemoryTaskGraphRepository; beforeEach(() => { - rmdirSync(".cache/test/file-task-graph", { recursive: true }); repository = new InMemoryTaskGraphRepository(); }); diff --git a/packages/storage/src/bun/sqlite/SqliteTaskGraphRepository.ts b/packages/storage/src/bun/sqlite/SqliteTaskGraphRepository.ts index 6bf7c92..941880a 100644 --- a/packages/storage/src/bun/sqlite/SqliteTaskGraphRepository.ts +++ b/packages/storage/src/bun/sqlite/SqliteTaskGraphRepository.ts @@ -5,14 +5,14 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { TaskGraphJson, TaskGraphRepository } from "ellmers-core"; +import { TaskGraphRepository } from "ellmers-core"; import { SqliteKVRepository } from "./base/SqliteKVRepository"; export class SqliteTaskGraphRepository extends TaskGraphRepository { - kvRepository: SqliteKVRepository; + kvRepository: SqliteKVRepository; public type = "SqliteTaskGraphRepository" as const; constructor(dbOrPath: string) { super(); - this.kvRepository = new SqliteKVRepository(dbOrPath, "task_graphs"); + this.kvRepository = new SqliteKVRepository(dbOrPath, "task_graphs"); } } diff --git a/packages/storage/src/bun/sqlite/SqliteTaskOutputRepository.ts b/packages/storage/src/bun/sqlite/SqliteTaskOutputRepository.ts index c8577ed..2ea177e 100644 --- a/packages/storage/src/bun/sqlite/SqliteTaskOutputRepository.ts +++ b/packages/storage/src/bun/sqlite/SqliteTaskOutputRepository.ts @@ -5,18 +5,27 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { TaskOutputDiscriminator, TaskOutputRepository, TaskInput, TaskOutput } from "ellmers-core"; +import { + TaskOutputPrimaryKeySchema, + TaskOutputRepository, + TaskOutputPrimaryKey, + DefaultValueType, +} from "ellmers-core"; import { SqliteKVRepository } from "./base/SqliteKVRepository"; export class SqliteTaskOutputRepository extends TaskOutputRepository { - kvRepository: SqliteKVRepository; + kvRepository: SqliteKVRepository< + TaskOutputPrimaryKey, + DefaultValueType, + typeof TaskOutputPrimaryKeySchema + >; public type = "SqliteTaskOutputRepository" as const; constructor(dbOrPath: string) { super(); this.kvRepository = new SqliteKVRepository< - TaskInput, - TaskOutput, - typeof TaskOutputDiscriminator - >(dbOrPath, "task_outputs", TaskOutputDiscriminator); + TaskOutputPrimaryKey, + DefaultValueType, + typeof TaskOutputPrimaryKeySchema + >(dbOrPath, "task_outputs", TaskOutputPrimaryKeySchema); } } diff --git a/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts b/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts index 50a70a7..47257eb 100644 --- a/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts +++ b/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts @@ -6,55 +6,83 @@ // ******************************************************************************* import { Database } from "bun:sqlite"; -import { DiscriminatorSchema, KVRepository } from "ellmers-core"; -import { makeFingerprint } from "../../../util/Misc"; +import { + BaseValueSchema, + BasicKeyType, + BasePrimaryKeySchema, + DefaultValueType, + DefaultValueSchema, + DefaultPrimaryKeyType, + DefaultPrimaryKeySchema, + KVRepository, + BasicValueType, +} from "ellmers-core"; + +type SQLiteValueTypes = string | number | boolean | null; + // SqliteKVRepository is a key-value store that uses SQLite as the backend for -// in app data. It supports discriminators. +// in app data. export class SqliteKVRepository< - Key = string, - Value = string, - Discriminator extends DiscriminatorSchema = DiscriminatorSchema -> extends KVRepository { + Key extends Record = DefaultPrimaryKeyType, + Value extends Record = DefaultValueType, + PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, + ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, + Combined extends Key & Value = Key & Value +> extends KVRepository { private db: Database; constructor( dbOrPath: string, public table: string = "kv_store", - discriminatorsSchema: Discriminator = {} as Discriminator + primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, + valueSchema: ValueSchema = DefaultValueSchema as ValueSchema ) { - super(); + super(primaryKeySchema, valueSchema); if (typeof dbOrPath === "string") { this.db = new Database(dbOrPath); } else { this.db = dbOrPath; } - this.discriminatorsSchema = discriminatorsSchema; this.setupDatabase(); } - private setupDatabase(): void { - this.db.exec(` - CREATE TABLE IF NOT EXISTS ${this.table} ( - ${this.constructDiscriminatorColumns()} - key TEXT NOT NULL, - value TEXT NOT NULL, + public setupDatabase(): void { + const sql = ` + CREATE TABLE IF NOT EXISTS \`${this.table}\` ( + ${this.constructPrimaryKeyColumns()}, + ${this.constructValueColumns()}, PRIMARY KEY (${this.primaryKeyColumnList()}) ) - `); + `; + this.db.exec(sql); } - private constructDiscriminatorColumns(): string { - const cols = Object.entries(this.discriminatorsSchema) + private constructPrimaryKeyColumns(): string { + const cols = Object.entries(this.primaryKeySchema) .map(([key, type]) => { // Convert the provided type to a SQL type, assuming simple mappings; adjust as necessary const sqlType = this.mapTypeToSQL(type); - return `${key} ${sqlType} NOT NULL`; + return `\`${key}\` ${sqlType} NOT NULL`; }) .join(", "); - if (cols.length > 0) { - return `${cols}, `; - } - return ""; + return cols; + } + + private constructValueColumns(): string { + const cols = Object.entries(this.valueSchema) + .map(([key, type]) => { + const sqlType = this.mapTypeToSQL(type); + return `\`${key}\` ${sqlType} NULL`; + }) + .join(", "); + return cols; + } + + protected primaryKeyColumnList(): string { + return "`" + this.primaryKeyColumns().join("`, `") + "`"; + } + protected valueColumnList(): string { + return "`" + this.valueColumns().join("`, `") + "`"; } private mapTypeToSQL(type: string): string { @@ -70,44 +98,92 @@ export class SqliteKVRepository< } } - async put(keySimpleOrObject: Key, value: Value): Promise { - const { discriminators, key } = this.extractDiscriminators(keySimpleOrObject); - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); - const stmt = this.db.prepare(` - INSERT OR REPLACE INTO ${this.table} (${this.primaryKeyColumnList()}, value) - VALUES (${this.primaryKeyColumns().map((i) => "?")}, ?) - `); - const values = Object.values(discriminators).concat(id, JSON.stringify(value)); - stmt.run(...values); - this.emit("put", id, discriminators); + // JS objects are not ordered, so we need to convert them to an ordered array + // so that we can use them as parameters in a SQL query + // we will order base on the valueSchema + getValueAsOrderedArray(value: Value): BasicValueType[] { + const orderedParams: BasicValueType[] = []; + // Iterate through valueSchema to maintain consistent order + for (const [key, type] of Object.entries(this.valueSchema)) { + if (key in value) { + orderedParams.push(value[key]); + } else { + throw new Error(`Missing required value field: ${key}`); + } + } + return orderedParams; } - async get(keySimpleOrObject: Key): Promise { - const { discriminators, key } = this.extractDiscriminators(keySimpleOrObject); - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); + // JS objects are not ordered, so we need to convert them to an ordered array + // so that we can use them as parameters in a SQL query + // we will order base on the primaryKeySchema + getPrimaryKeyAsOrderedArray(key: Key): BasicKeyType[] { + const orderedParams: BasicKeyType[] = []; + // Iterate through primaryKeySchema to maintain consistent order + for (const [k, type] of Object.entries(this.primaryKeySchema)) { + if (k in key) { + orderedParams.push(key[k]); + } else { + throw new Error(`Missing required primary key field: ${k}`); + } + } + return orderedParams; + } - const whereClauses = this.primaryKeyColumns() - .map((discriminatorKey) => `${discriminatorKey} = ?`) - .join(" AND "); + async putKeyValue(key: Key, value: Value): Promise { + const sql = ` + INSERT OR REPLACE INTO ${ + this.table + } (${this.primaryKeyColumnList()}, ${this.valueColumnList()}) + VALUES ( + ${this.primaryKeyColumns().map((i) => "?")}, + ${this.valueColumns().map((i) => "?")} + ) + `; + const stmt = this.db.prepare(sql); - const stmt = this.db.prepare<{ value: string }, [key: string]>(` - SELECT value FROM ${this.table} WHERE ${whereClauses} - `); + const primaryKeyParams = this.getPrimaryKeyAsOrderedArray(key); + const valueParams = this.getValueAsOrderedArray(value); + const params = [...primaryKeyParams, ...valueParams]; - const values = Object.values(discriminators).concat(id); + const result = stmt.run(...params); - const row = stmt.get(...(values as [string])) as { value: string } | undefined; - if (row) { - this.emit("get", id, discriminators); - return JSON.parse(row.value) as Value; + this.emit("put", key); + } + + async getKeyValue(key: Key): Promise { + const whereClauses = (this.primaryKeyColumns() as string[]) + .map((key) => `\`${key}\` = ?`) + .join(" AND "); + + const sql = ` + SELECT ${this.valueColumnList()} FROM ${this.table} WHERE ${whereClauses} + `; + // const sql = `SELECT * FROM ${this.table} `; + const stmt = this.db.prepare(sql); + const params = this.getPrimaryKeyAsOrderedArray(key); + const value = stmt.get(...params); + if (value) { + this.emit("get", key, value); + return value; } else { return undefined; } } - async clear(): Promise { + async deleteKeyValue(key: Key): Promise { + const whereClauses = (this.primaryKeyColumns() as string[]) + .map((key) => `${key} = ?`) + .join(" AND "); + const params = this.getPrimaryKeyAsOrderedArray(key); + const stmt = this.db.prepare(`DELETE FROM ${this.table} WHERE ${whereClauses}`); + stmt.run(...params); + this.emit("delete", key); + } + + async deleteAll(): Promise { this.db.exec(`DELETE FROM ${this.table}`); - this.emit("clear"); + this.emit("clearall"); } async size(): Promise { diff --git a/packages/storage/src/bun/sqlite/test/SqliteKVRepository.test.ts b/packages/storage/src/bun/sqlite/test/SqliteKVRepository.test.ts new file mode 100644 index 0000000..ca6d397 --- /dev/null +++ b/packages/storage/src/bun/sqlite/test/SqliteKVRepository.test.ts @@ -0,0 +1,78 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { describe, expect, it, beforeEach } from "bun:test"; +import { SqliteKVRepository } from "../base/SqliteKVRepository"; +import { BaseValueSchema, BasePrimaryKeySchema } from "ellmers-core"; + +type PrimaryKey = { + name: string; + type: string; +}; +type Value = { + option: string; + success: boolean; +}; + +export const PrimaryKeySchema: BasePrimaryKeySchema = { name: "string", type: "string" } as const; +export const ValueSchema: BaseValueSchema = { option: "string", success: "boolean" } as const; + +describe("SqliteKVRepository", () => { + describe("with default schemas (key and value)", () => { + let repository: SqliteKVRepository; + + beforeEach(() => { + repository = new SqliteKVRepository(":memory:"); + }); + + it("should store and retrieve values for a key", async () => { + const key = "key"; + const value = "value"; + await repository.put(key, value); + const output = await repository.get(key); + + expect(output).toEqual(value); + }); + it("should get undefined for a key that doesn't exist", async () => { + const key = "key"; + const value = "value"; + await repository.put(key, value); + const output = await repository.get("not-a-key"); + + expect(output == undefined).toEqual(true); + }); + }); + + describe("with complex schemas", () => { + let repository: SqliteKVRepository; + + beforeEach(() => { + repository = new SqliteKVRepository( + ":memory:", + "complex_store", + PrimaryKeySchema, + ValueSchema + ); + }); + + it("should store and retrieve values for a key", async () => { + const key = { name: "key", type: "string" }; + const value = { option: "value", success: true }; + await repository.putKeyValue(key, value); + const output = await repository.getKeyValue(key); + + expect(output?.option).toEqual("value"); + expect(!!output?.success).toEqual(true); // TODO need some conversion to boolean from 1 + }); + it("should get undefined for a key that doesn't exist", async () => { + const key = { name: "key", type: "string" }; + const output = await repository.get(key); + + expect(output == undefined).toEqual(true); + }); + }); +}); diff --git a/packages/storage/src/node/filesystem/FileTaskGraphRepository.ts b/packages/storage/src/node/filesystem/FileTaskGraphRepository.ts index 5dac80b..378974d 100644 --- a/packages/storage/src/node/filesystem/FileTaskGraphRepository.ts +++ b/packages/storage/src/node/filesystem/FileTaskGraphRepository.ts @@ -5,14 +5,14 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { TaskGraphJson, TaskGraphRepository } from "ellmers-core"; +import { TaskGraphRepository } from "ellmers-core"; import { FileKVRepository } from "./base/FileKVRepository"; export class FileTaskGraphRepository extends TaskGraphRepository { - kvRepository: FileKVRepository; + kvRepository: FileKVRepository; public type = "FileTaskGraphRepository" as const; constructor(folderPath: string) { super(); - this.kvRepository = new FileKVRepository(folderPath); + this.kvRepository = new FileKVRepository(folderPath); } } diff --git a/packages/storage/src/node/filesystem/FileTaskOutputRepository.ts b/packages/storage/src/node/filesystem/FileTaskOutputRepository.ts index 054fc5b..af762ae 100644 --- a/packages/storage/src/node/filesystem/FileTaskOutputRepository.ts +++ b/packages/storage/src/node/filesystem/FileTaskOutputRepository.ts @@ -5,17 +5,28 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { TaskInput, TaskOutput, TaskOutputDiscriminator, TaskOutputRepository } from "ellmers-core"; +import { + DefaultValueType, + TaskInput, + TaskOutputPrimaryKey, + TaskOutputPrimaryKeySchema, + TaskOutputRepository, +} from "ellmers-core"; import { FileKVRepository } from "./base/FileKVRepository"; export class FileTaskOutputRepository extends TaskOutputRepository { - kvRepository: FileKVRepository; + kvRepository: FileKVRepository< + TaskOutputPrimaryKey, + DefaultValueType, + typeof TaskOutputPrimaryKeySchema + >; public type = "FileTaskOutputRepository" as const; constructor(folderPath: string) { super(); - this.kvRepository = new FileKVRepository( - folderPath, - TaskOutputDiscriminator - ); + this.kvRepository = new FileKVRepository< + TaskOutputPrimaryKey, + DefaultValueType, + typeof TaskOutputPrimaryKeySchema + >(folderPath, TaskOutputPrimaryKeySchema); } } diff --git a/packages/storage/src/node/filesystem/base/FileKVRepository.ts b/packages/storage/src/node/filesystem/base/FileKVRepository.ts index a0be385..fd7747a 100644 --- a/packages/storage/src/node/filesystem/base/FileKVRepository.ts +++ b/packages/storage/src/node/filesystem/base/FileKVRepository.ts @@ -6,55 +6,79 @@ // ******************************************************************************* import path from "node:path"; -import { readFile, writeFile, unlink, mkdir } from "node:fs/promises"; -import { DiscriminatorSchema, KVRepository } from "ellmers-core"; -import { makeFingerprint } from "../../../util/Misc"; +import { readFile, writeFile, rm } from "node:fs/promises"; +import { mkdirSync } from "node:fs"; import { glob } from "glob"; +import { + BaseValueSchema, + BasePrimaryKeySchema, + BasicKeyType, + DefaultValueType, + DefaultValueSchema, + DefaultPrimaryKeyType, + DefaultPrimaryKeySchema, + KVRepository, +} from "ellmers-core"; // FileKVRepository is a key-value store that uses the file system as the backend for -// simple scenarios. It does support discriminators. +// simple scenarios. export class FileKVRepository< - Key = string, - Value = string, - Discriminator extends DiscriminatorSchema = DiscriminatorSchema -> extends KVRepository { + Key extends Record = DefaultPrimaryKeyType, + Value extends Record = DefaultValueType, + PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, + ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, + Combined extends Key & Value = Key & Value +> extends KVRepository { private folderPath: string; - constructor(folderPath: string, discriminatorsSchema: Discriminator = {} as Discriminator) { - super(); - this.discriminatorsSchema = discriminatorsSchema; - this.folderPath = folderPath; - mkdir(this.folderPath, { recursive: true }); + constructor( + folderPath: string, + primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, + valueSchema: ValueSchema = DefaultValueSchema as ValueSchema + ) { + super(primaryKeySchema, valueSchema); + this.folderPath = path.dirname(folderPath); + mkdirSync(this.folderPath, { recursive: true }); } - async put(keySimpleOrObject: Key, value: Value): Promise { - const { discriminators, key } = this.extractDiscriminators(keySimpleOrObject); - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); - const filePath = await this.getFilePath(key, discriminators); - await writeFile(filePath, JSON.stringify(value)); + async putKeyValue(key: Key, value: Value): Promise { + const filePath = await this.getFilePath(key); + try { + await writeFile(filePath, JSON.stringify(value)); + } catch (error) { + console.error("Error writing file", filePath, error); + } this.emit("put", key); } - async get(keySimpleOrObject: Key): Promise { - const { discriminators, key } = this.extractDiscriminators(keySimpleOrObject); - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); - const filePath = await this.getFilePath(key, discriminators); + async getKeyValue(key: Key): Promise { + const filePath = await this.getFilePath(key); try { const data = await readFile(filePath, "utf-8"); - this.emit("get", key); - return JSON.parse(data); + const value = JSON.parse(data) as Value; + this.emit("get", key, value); + return value; } catch (error) { + // console.info("Error getting file (may not exist)", filePath); return undefined; // File not found or read error } } - async clear(): Promise { + async deleteKeyValue(key: Key): Promise { + const filePath = await this.getFilePath(key); + try { + await rm(filePath); + } catch (error) { + // console.error("Error deleting file", filePath, error); + } + this.emit("delete", key); + } + + async deleteAll(): Promise { // Delete all files in the folder ending in .json - const globPattern = path.join(this.folderPath, "*.json"); - const filesToDelete = await glob(globPattern); - await Promise.all(filesToDelete.map((file) => unlink(file))); - this.emit("clear"); + await rm(this.folderPath, { recursive: true, force: true }); + this.emit("clearall"); } async size(): Promise { @@ -64,12 +88,9 @@ export class FileKVRepository< return files.length; } - private async getFilePath( - key: Key, - discriminators: Record - ): Promise { - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); - const filename = Object.values(discriminators).concat(id).join("_"); - return path.join(this.folderPath, `${filename}.json`); + private async getFilePath(key: Key | BasicKeyType): Promise { + const filename = await this.getKeyAsIdString(key); + const fullPath = path.join(this.folderPath, `${filename}.json`); + return fullPath; } } diff --git a/packages/storage/src/node/filesystem/test/FileKVRepository.test.ts b/packages/storage/src/node/filesystem/test/FileKVRepository.test.ts new file mode 100644 index 0000000..a9dea4f --- /dev/null +++ b/packages/storage/src/node/filesystem/test/FileKVRepository.test.ts @@ -0,0 +1,94 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { describe, expect, it, beforeEach, afterEach } from "bun:test"; +import { rmdirSync } from "fs"; +import { FileKVRepository } from "../base/FileKVRepository"; +import { BaseValueSchema, BasePrimaryKeySchema } from "ellmers-core"; + +type PrimaryKey = { + name: string; + type: string; +}; +type Value = { + option: string; + success: boolean; +}; + +export const PrimaryKeySchema: BasePrimaryKeySchema = { name: "string", type: "string" } as const; +export const ValueSchema: BaseValueSchema = { option: "string", success: "boolean" } as const; + +const testDir = ".cache/test/testing"; + +describe("FileKVRepository", () => { + let repository: FileKVRepository; + rmdirSync(testDir, { recursive: true }); + + beforeEach(() => { + repository = new FileKVRepository(testDir, {}); + }); + afterEach(() => { + repository.deleteAll(); + }); + + describe("with default schemas (key and value)", () => { + let repository: FileKVRepository; + + beforeEach(() => { + repository = new FileKVRepository(testDir); + }); + + it("should store and retrieve values for a key", async () => { + const key = "key"; + const value = "value"; + await repository.put(key, value); + const output = await repository.get(key); + + expect(output).toEqual(value); + }); + it("should get undefined for a key that doesn't exist", async () => { + const key = "key"; + const value = "value"; + await repository.put(key, value); + const output = await repository.get("not-a-key"); + + expect(output == undefined).toEqual(true); + }); + }); + + describe("with complex schemas", () => { + let repository: FileKVRepository; + + beforeEach(async () => { + repository = new FileKVRepository(testDir, PrimaryKeySchema, ValueSchema); + }); + afterEach(async () => { + await repository.deleteAll(); + }); + + it("should store and retrieve values for a key", async () => { + const key = { name: "key", type: "string" }; + const value = { option: "value", success: true }; + await repository.put(key, value); + const output = await repository.getKeyValue(key); + + expect(output?.option).toEqual("value"); + expect(!!output?.success).toEqual(true); // TODO need some conversion to boolean from 1 + + await repository.delete(key); + + const output2 = await repository.get(key); + expect(output2 == undefined).toEqual(true); + }); + it("should get undefined for a key that doesn't exist", async () => { + const key = { name: "key-unknown", type: "string" }; + const output = await repository.get(key); + + expect(output == undefined).toEqual(true); + }); + }); +}); diff --git a/packages/storage/src/node/postgres/PostgresTaskGraphRepository.ts b/packages/storage/src/node/postgres/PostgresTaskGraphRepository.ts index f0e3ce7..855e44a 100644 --- a/packages/storage/src/node/postgres/PostgresTaskGraphRepository.ts +++ b/packages/storage/src/node/postgres/PostgresTaskGraphRepository.ts @@ -5,17 +5,14 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { TaskGraphJson, TaskGraphRepository } from "ellmers-core"; +import { TaskGraphRepository } from "ellmers-core"; import { PostgresKVRepository } from "./base/PostgresKVRepository"; export class PostgresTaskGraphRepository extends TaskGraphRepository { - kvRepository: PostgresKVRepository; + kvRepository: PostgresKVRepository; public type = "PostgresTaskGraphRepository" as const; constructor(connectionString: string) { super(); - this.kvRepository = new PostgresKVRepository( - connectionString, - "task_graphs" - ); + this.kvRepository = new PostgresKVRepository(connectionString, "task_graphs"); } } diff --git a/packages/storage/src/node/postgres/PostgresTaskOutputRepository.ts b/packages/storage/src/node/postgres/PostgresTaskOutputRepository.ts index cb75b2c..e69e24b 100644 --- a/packages/storage/src/node/postgres/PostgresTaskOutputRepository.ts +++ b/packages/storage/src/node/postgres/PostgresTaskOutputRepository.ts @@ -5,18 +5,27 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { TaskInput, TaskOutput, TaskOutputDiscriminator, TaskOutputRepository } from "ellmers-core"; +import { + DefaultValueType, + TaskOutputPrimaryKey, + TaskOutputPrimaryKeySchema, + TaskOutputRepository, +} from "ellmers-core"; import { PostgresKVRepository } from "./base/PostgresKVRepository"; export class PostgresTaskOutputRepository extends TaskOutputRepository { - kvRepository: PostgresKVRepository; + kvRepository: PostgresKVRepository< + TaskOutputPrimaryKey, + DefaultValueType, + typeof TaskOutputPrimaryKeySchema + >; public type = "PostgresTaskOutputRepository" as const; constructor(connectionString: string) { super(); this.kvRepository = new PostgresKVRepository< - TaskInput, - TaskOutput, - typeof TaskOutputDiscriminator - >(connectionString, "task_outputs", TaskOutputDiscriminator); + TaskOutputPrimaryKey, + DefaultValueType, + typeof TaskOutputPrimaryKeySchema + >(connectionString, "task_outputs", TaskOutputPrimaryKeySchema); } } diff --git a/packages/storage/src/node/postgres/base/PostgresKVRepository.ts b/packages/storage/src/node/postgres/base/PostgresKVRepository.ts index 3973cea..6baa1d6 100644 --- a/packages/storage/src/node/postgres/base/PostgresKVRepository.ts +++ b/packages/storage/src/node/postgres/base/PostgresKVRepository.ts @@ -6,53 +6,77 @@ // ******************************************************************************* import { Pool } from "pg"; -import { DiscriminatorSchema, KVRepository } from "ellmers-core"; -import { makeFingerprint } from "../../../util/Misc"; +import { + BaseValueSchema, + BasePrimaryKeySchema, + BasicKeyType, + BasicValueType, + DefaultValueSchema, + DefaultPrimaryKeySchema, + DefaultPrimaryKeyType, + DefaultValueType, + KVRepository, +} from "ellmers-core"; // PostgresKVRepository is a key-value store that uses PostgreSQL as the backend for // multi-user scenarios. It supports discriminators. export class PostgresKVRepository< - Key = string, - Value = string, - Discriminator extends DiscriminatorSchema = DiscriminatorSchema -> extends KVRepository { + Key extends Record = DefaultPrimaryKeyType, + Value extends Record = DefaultValueType, + PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, + ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, + Combined extends Key & Value = Key & Value +> extends KVRepository { private pool: Pool; constructor( connectionString: string, public table: string = "kv_store", - discriminatorsSchema: Discriminator = {} as Discriminator + primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, + valueSchema: ValueSchema = DefaultValueSchema as ValueSchema ) { - super(); - this.discriminatorsSchema = discriminatorsSchema; + super(primaryKeySchema, valueSchema); this.pool = new Pool({ connectionString }); - this.setupDatabase(table); + this.setupDatabase(); } - private async setupDatabase(table: string): Promise { + private async setupDatabase(): Promise { await this.pool.query(` - CREATE TABLE IF NOT EXISTS ${this.table} ( - ${this.constructDiscriminatorColumns()} - key TEXT NOT NULL, - value JSONB NOT NULL, + CREATE TABLE IF NOT EXISTS \`${this.table}\` ( + ${this.constructPrimaryKeyColumns()}, + ${this.constructValueColumns()}, PRIMARY KEY (${this.primaryKeyColumnList()}) ) `); } - private constructDiscriminatorColumns(): string { - const cols = Object.entries(this.discriminatorsSchema) + private constructPrimaryKeyColumns(): string { + const cols = Object.entries(this.primaryKeySchema) .map(([key, type]) => { // Convert the provided type to a SQL type, assuming simple mappings; adjust as necessary const sqlType = this.mapTypeToSQL(type); - return `${key} ${sqlType} NOT NULL`; + return `\`${key}\` ${sqlType} NOT NULL`; }) .join(", "); - if (cols.length > 0) { - return `${cols}, `; - } - return ""; + return cols; + } + + private constructValueColumns(): string { + const cols = Object.entries(this.valueSchema) + .map(([key, type]) => { + const sqlType = this.mapTypeToSQL(type); + return `\`${key}\` ${sqlType} NULL`; + }) + .join(", "); + return cols; + } + + protected primaryKeyColumnList(): string { + return "`" + this.primaryKeyColumns().join("`, `") + "`"; + } + protected valueColumnList(): string { + return "`" + this.valueColumns().join("`, `") + "`"; } private mapTypeToSQL(type: string): string { @@ -68,46 +92,88 @@ export class PostgresKVRepository< } } - async put(keySimpleOrObject: Key, value: Value): Promise { - const { discriminators, key } = this.extractDiscriminators(keySimpleOrObject); - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); - const values = Object.values(discriminators).concat(id, JSON.stringify(value)); - await this.pool.query( - `INSERT INTO ${this.table} (${this.primaryKeyColumnList()}, value) - VALUES (${this.primaryKeyColumns().map((i) => "?")}, ?) - ON CONFLICT (key) DO UPDATE - SET value = EXCLUDED.value`, - values - ); - this.emit("put", id, discriminators); + // JS objects are not ordered, so we need to convert them to an ordered array + // so that we can use them as parameters in a SQL query + // we will order base on the valueSchema + getValueAsOrderedArray(value: Value): BasicValueType[] { + const orderedParams: BasicValueType[] = []; + // Iterate through valueSchema to maintain consistent order + for (const [key, type] of Object.entries(this.valueSchema)) { + orderedParams.push(value[key] ?? null); + } + return orderedParams; } - async get(keySimpleOrObject: Key): Promise { - const { discriminators, key } = this.extractDiscriminators(keySimpleOrObject); - const id = typeof key === "object" ? await makeFingerprint(key) : String(key); + // JS objects are not ordered, so we need to convert them to an ordered array + // so that we can use them as parameters in a SQL query + // we will order base on the primaryKeySchema + getPrimaryKeyAsOrderedArray(key: Key): BasicKeyType[] { + const orderedParams: BasicKeyType[] = []; + // Iterate through primaryKeySchema to maintain consistent order + for (const [k, type] of Object.entries(this.primaryKeySchema)) { + if (k in key) { + orderedParams.push(key[k]); + } else { + throw new Error(`Missing required primary key field: ${k}`); + } + } + return orderedParams; + } + + async putKeyValue(key: Key, value: Value): Promise { + const sql = ` + INSERT INTO \`${this.table}\` ( + ${this.primaryKeyColumnList()}, + ${this.valueColumnList()} + ) + VALUES ( + ${this.primaryKeyColumns().map((i) => "?")} + ) + ON CONFLICT (${this.primaryKeyColumnList()}) DO UPDATE + SET + ${(this.valueColumns() as string[]).map((col) => `\`${col}\` = EXCLUDED.\`${col}\``).join(", ")} + `; + + const primaryKeyParams = this.getPrimaryKeyAsOrderedArray(key); + const valueParams = this.getValueAsOrderedArray(value); + const params = [...primaryKeyParams, ...valueParams]; + await this.pool.query(sql, params); + this.emit("put", key); + } - const whereClauses = this.primaryKeyColumns() - .map((discriminatorKey, i) => `${discriminatorKey} = $${i + 1}`) + async getKeyValue(key: Key): Promise { + const whereClauses = (this.primaryKeyColumns() as string[]) + .map((discriminatorKey, i) => `\`${discriminatorKey}\` = $${i + 1}`) .join(" AND "); - const values = Object.values(discriminators).concat(id); + const params = this.getPrimaryKeyAsOrderedArray(key); const result = await this.pool.query( - `SELECT value FROM ${this.table} WHERE ${whereClauses}`, - values + `SELECT ${this.valueColumnList()} FROM \`${this.table}\` WHERE ${whereClauses}`, + params ); if (result.rows.length > 0) { - this.emit("get", id, discriminators); - return result.rows[0].value as Value; + this.emit("get", key); + return result.rows[0] as Value; } else { return undefined; } } - async clear(): Promise { - await this.pool.query(`DELETE FROM ${this.table}`); - this.emit("clear"); + async deleteKeyValue(key: Key): Promise { + const whereClauses = (this.primaryKeyColumns() as string[]) + .map((key, i) => `\`${key}\` = $${i + 1}`) + .join(" AND "); + + const params = this.getPrimaryKeyAsOrderedArray(key); + await this.pool.query(`DELETE FROM \`${this.table}\` WHERE ${whereClauses}`, params); + this.emit("delete", key); + } + + async deleteAll(): Promise { + await this.pool.query(`DELETE FROM \`${this.table}\``); + this.emit("clearall"); } async size(): Promise { From 87f0814716dfb68ff5c4c296f87a003c4e7211da Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Wed, 15 Jan 2025 21:23:10 -0800 Subject: [PATCH 04/12] wip: model repository [broken] --- examples/cli/src/TaskCLI.ts | 10 +- examples/web/src/App.tsx | 6 +- examples/web/src/QueueSatus.tsx | 4 +- .../src/ggml/model/GgmlLocalModel.ts | 9 +- .../hf-transformers/bindings/registerTasks.ts | 16 +-- .../model/ONNXTransformerJsModel.ts | 10 +- .../test/HFTransformersBinding.test.ts | 10 +- .../tf-mediapipe/bindings/registerTasks.ts | 6 +- .../src/tf-mediapipe/model/MediaPipeModel.ts | 4 +- .../test/TfMediaPipeBinding.test.ts | 10 +- packages/ai/src/index.ts | 2 +- packages/ai/src/model/Model.ts | 114 +++++++++++++----- packages/ai/src/model/ModelRepository.ts | 51 ++++++++ packages/ai/src/provider/ProviderRegistry.ts | 14 +-- packages/storage/package.json | 7 +- .../inmemory/InMemoryModelRepository.ts} | 22 ++-- .../src/bun/sqlite/SqliteModelRepository.ts | 22 ++++ packages/storage/tsconfig.json | 2 +- 18 files changed, 219 insertions(+), 100 deletions(-) create mode 100644 packages/ai/src/model/ModelRepository.ts rename packages/{ai/src/model/InMemoryStorage.ts => storage/src/browser/inmemory/InMemoryModelRepository.ts} (50%) create mode 100644 packages/storage/src/bun/sqlite/SqliteModelRepository.ts diff --git a/examples/cli/src/TaskCLI.ts b/examples/cli/src/TaskCLI.ts index d29cb28..4501f46 100644 --- a/examples/cli/src/TaskCLI.ts +++ b/examples/cli/src/TaskCLI.ts @@ -13,7 +13,6 @@ import { TaskGraph, JsonTask, TaskGraphBuilder, JsonTaskItem } from "ellmers-cor import { DownloadModelTask, DownloadModelCompoundTask, - findAllModels, findModelByName, findModelByUseCase, ModelUseCaseEnum, @@ -24,9 +23,8 @@ export function AddBaseCommands(program: Command) { program .command("download") .description("download models") - .option("--model ", "model to download") + .requiredOption("--model ", "model to download") .action(async (options) => { - const models = findAllModels(); const graph = new TaskGraph(); if (options.model) { const model = findModelByName(options.model); @@ -35,12 +33,6 @@ export function AddBaseCommands(program: Command) { } else { program.error(`Unknown model ${options.model}`); } - } else { - graph.addTask( - new DownloadModelCompoundTask({ - input: { model: models.map((m) => m.name) }, - }) - ); } await runTask(graph); }); diff --git a/examples/web/src/App.tsx b/examples/web/src/App.tsx index 4a9b5b2..decd208 100644 --- a/examples/web/src/App.tsx +++ b/examples/web/src/App.tsx @@ -22,7 +22,7 @@ import { OutputRepositoryStatus } from "./OutputRepositoryStatus"; import { GraphStoreStatus } from "./GraphStoreStatus"; import { InMemoryJobQueue } from "ellmers-storage/inmemory"; import { registerHuggingfaceLocalTasks } from "ellmers-ai-provider/hf-transformers/browser"; -import { getProviderRegistry, ModelProcessorEnum } from "ellmers-ai"; +import { getProviderRegistry, ModelProviderEnum } from "ellmers-ai"; import { registerMediaPipeTfJsLocalTasks } from "ellmers-ai-provider/tf-mediapipe/browser"; import "ellmers-task"; import "ellmers-test"; @@ -31,13 +31,13 @@ const ProviderRegistry = getProviderRegistry(); registerHuggingfaceLocalTasks(); ProviderRegistry.registerQueue( - ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, + ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, new InMemoryJobQueue("local_hft", new ConcurrencyLimiter(1, 10), 10) ); registerMediaPipeTfJsLocalTasks(); ProviderRegistry.registerQueue( - ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, + ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL, new InMemoryJobQueue("local_mp", new ConcurrencyLimiter(1, 10), 10) ); diff --git a/examples/web/src/QueueSatus.tsx b/examples/web/src/QueueSatus.tsx index b12c738..4bc7c7e 100644 --- a/examples/web/src/QueueSatus.tsx +++ b/examples/web/src/QueueSatus.tsx @@ -1,8 +1,8 @@ import { JobStatus } from "ellmers-core"; -import { ModelProcessorEnum, getProviderRegistry } from "ellmers-ai"; +import { ModelProviderEnum, getProviderRegistry } from "ellmers-ai"; import { useCallback, useEffect, useState } from "react"; -export function QueueStatus({ queueType }: { queueType: ModelProcessorEnum }) { +export function QueueStatus({ queueType }: { queueType: ModelProviderEnum }) { const queue = getProviderRegistry().getQueue(queueType); const [pending, setPending] = useState(0); const [processing, setProcessing] = useState(0); diff --git a/packages/ai-provider/src/ggml/model/GgmlLocalModel.ts b/packages/ai-provider/src/ggml/model/GgmlLocalModel.ts index e24729f..9093ede 100644 --- a/packages/ai-provider/src/ggml/model/GgmlLocalModel.ts +++ b/packages/ai-provider/src/ggml/model/GgmlLocalModel.ts @@ -5,16 +5,11 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { - Model, - ModelOptions, - ModelProcessorEnum, - ModelUseCaseEnum, -} from "../../../../ai/src/model/Model"; +import { Model, ModelProviderEnum, ModelUseCaseEnum } from "../../../../ai/src/model/Model"; export class GgmlLocalModel extends Model { constructor(name: string, useCase: ModelUseCaseEnum[], options?: ModelOptions) { super(name, useCase, options); } - readonly type = ModelProcessorEnum.LOCAL_LLAMACPP; + readonly type = ModelProviderEnum.LOCAL_LLAMACPP; } diff --git a/packages/ai-provider/src/hf-transformers/bindings/registerTasks.ts b/packages/ai-provider/src/hf-transformers/bindings/registerTasks.ts index 9054c7a..64054a5 100644 --- a/packages/ai-provider/src/hf-transformers/bindings/registerTasks.ts +++ b/packages/ai-provider/src/hf-transformers/bindings/registerTasks.ts @@ -1,5 +1,5 @@ import { - ModelProcessorEnum, + ModelProviderEnum, getProviderRegistry, DownloadModelTask, TextEmbeddingTask, @@ -24,43 +24,43 @@ export async function registerHuggingfaceLocalTasks() { ProviderRegistry.registerRunFn( DownloadModelTask.type, - ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, + ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_DownloadRun ); ProviderRegistry.registerRunFn( TextEmbeddingTask.type, - ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, + ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_EmbeddingRun ); ProviderRegistry.registerRunFn( TextGenerationTask.type, - ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, + ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_TextGenerationRun ); ProviderRegistry.registerRunFn( TextTranslationTask.type, - ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, + ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_TextTranslationRun ); ProviderRegistry.registerRunFn( TextRewriterTask.type, - ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, + ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_TextRewriterRun ); ProviderRegistry.registerRunFn( TextSummaryTask.type, - ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, + ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_TextSummaryRun ); ProviderRegistry.registerRunFn( TextQuestionAnswerTask.type, - ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, + ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_TextQuestionAnswerRun ); } diff --git a/packages/ai-provider/src/hf-transformers/model/ONNXTransformerJsModel.ts b/packages/ai-provider/src/hf-transformers/model/ONNXTransformerJsModel.ts index ec3e154..6f4fa4b 100644 --- a/packages/ai-provider/src/hf-transformers/model/ONNXTransformerJsModel.ts +++ b/packages/ai-provider/src/hf-transformers/model/ONNXTransformerJsModel.ts @@ -5,9 +5,9 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { Model, ModelProcessorEnum, ModelUseCaseEnum, type ModelOptions } from "ellmers-ai"; +import { Model, ModelDetail, ModelProviderEnum, ModelUseCaseEnum } from "ellmers-ai"; -export enum DATA_TYPES { +export enum QUANTIZATION_DATA_TYPES { auto = "auto", // Auto-detect based on environment fp32 = "fp32", fp16 = "fp16", @@ -19,8 +19,8 @@ export enum DATA_TYPES { q4f16 = "q4f16", // fp16 model with int4 block weight quantization } -export interface ONNXTransformerJsModelOptions extends ModelOptions { - dtype?: DATA_TYPES | { [key: string]: DATA_TYPES }; +export interface ONNXTransformerJsModelOptions extends ModelDetail { + quantization?: QUANTIZATION_DATA_TYPES; } export class ONNXTransformerJsModel extends Model implements ONNXTransformerJsModelOptions { @@ -36,6 +36,6 @@ export class ONNXTransformerJsModel extends Model implements ONNXTransformerJsMo super(name, useCase, options); this.dtype = options?.dtype ?? DATA_TYPES.q8; } - readonly type = ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS; + readonly type = ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS; dtype?: DATA_TYPES | { [key: string]: DATA_TYPES } | undefined; } diff --git a/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts b/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts index 3c1dded..7d31de8 100644 --- a/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts +++ b/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts @@ -7,7 +7,7 @@ import { describe, expect, it } from "bun:test"; import { ConcurrencyLimiter, TaskGraphBuilder, TaskInput, TaskOutput } from "ellmers-core"; -import { getProviderRegistry, ModelProcessorEnum, ModelUseCaseEnum } from "ellmers-ai"; +import { getProviderRegistry, ModelProviderEnum, ModelUseCaseEnum } from "ellmers-ai"; import { InMemoryJobQueue } from "ellmers-storage/inmemory"; import { SqliteJobQueue } from "ellmers-storage/bun/sqlite"; import { registerHuggingfaceLocalTasks } from "../bindings/registerTasks"; @@ -33,8 +33,8 @@ describe("HFTransformersBinding", () => { new ConcurrencyLimiter(1, 10), 10 ); - providerRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); - const queue = providerRegistry.getQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS); + providerRegistry.registerQueue(ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); + const queue = providerRegistry.getQueue(ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS); expect(queue).toBeDefined(); expect(queue?.queue).toEqual(HFQUEUE); @@ -60,8 +60,8 @@ describe("HFTransformersBinding", () => { 10 ); jobQueue.ensureTableExists(); - providerRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); - const queue = providerRegistry.getQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS); + providerRegistry.registerQueue(ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); + const queue = providerRegistry.getQueue(ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS); expect(queue).toBeDefined(); expect(queue?.queue).toEqual(HFQUEUE); diff --git a/packages/ai-provider/src/tf-mediapipe/bindings/registerTasks.ts b/packages/ai-provider/src/tf-mediapipe/bindings/registerTasks.ts index 3c2ff3e..4b4277f 100644 --- a/packages/ai-provider/src/tf-mediapipe/bindings/registerTasks.ts +++ b/packages/ai-provider/src/tf-mediapipe/bindings/registerTasks.ts @@ -1,4 +1,4 @@ -import { ModelProcessorEnum, getProviderRegistry } from "ellmers-ai"; +import { ModelProviderEnum, getProviderRegistry } from "ellmers-ai"; import { DownloadModelTask, TextEmbeddingTask } from "ellmers-ai"; import { MediaPipeTfJsLocal_Download, @@ -10,13 +10,13 @@ export const registerMediaPipeTfJsLocalTasks = () => { ProviderRegistry.registerRunFn( DownloadModelTask.type, - ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, + ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL, MediaPipeTfJsLocal_Download ); ProviderRegistry.registerRunFn( TextEmbeddingTask.type, - ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, + ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL, MediaPipeTfJsLocal_Embedding ); }; diff --git a/packages/ai-provider/src/tf-mediapipe/model/MediaPipeModel.ts b/packages/ai-provider/src/tf-mediapipe/model/MediaPipeModel.ts index 5cb3c70..60e2584 100644 --- a/packages/ai-provider/src/tf-mediapipe/model/MediaPipeModel.ts +++ b/packages/ai-provider/src/tf-mediapipe/model/MediaPipeModel.ts @@ -5,7 +5,7 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { Model, ModelOptions, ModelProcessorEnum, ModelUseCaseEnum } from "ellmers-ai"; +import { Model, ModelOptions, ModelProviderEnum, ModelUseCaseEnum } from "ellmers-ai"; export class MediaPipeTfJsModel extends Model { constructor( @@ -16,5 +16,5 @@ export class MediaPipeTfJsModel extends Model { ) { super(name, useCase, options); } - readonly type = ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL; + readonly type = ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL; } diff --git a/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts b/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts index ee9ed8d..890f842 100644 --- a/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts +++ b/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts @@ -7,7 +7,7 @@ import { describe, expect, it } from "bun:test"; import { ConcurrencyLimiter, TaskGraphBuilder, TaskInput, TaskOutput } from "ellmers-core"; -import { getProviderRegistry, ModelProcessorEnum, ModelUseCaseEnum } from "ellmers-ai"; +import { getProviderRegistry, ModelProviderEnum, ModelUseCaseEnum } from "ellmers-ai"; import { InMemoryJobQueue } from "ellmers-storage/inmemory"; import { SqliteJobQueue } from "ellmers-storage/bun/sqlite"; import { registerMediaPipeTfJsLocalTasks } from "../bindings/registerTasks"; @@ -34,8 +34,8 @@ describe("TfMediaPipeBinding", () => { new ConcurrencyLimiter(1, 10), 10 ); - ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); - const queue = ProviderRegistry.getQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL); + ProviderRegistry.registerQueue(ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); + const queue = ProviderRegistry.getQueue(ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL); expect(queue).toBeDefined(); expect(queue?.queue).toEqual(TFQUEUE); @@ -68,8 +68,8 @@ describe("TfMediaPipeBinding", () => { 10 ); jobQueue.ensureTableExists(); - ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); - const queue = ProviderRegistry.getQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL); + ProviderRegistry.registerQueue(ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); + const queue = ProviderRegistry.getQueue(ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL); expect(queue).toBeDefined(); expect(queue?.queue).toEqual(TFQUEUE); diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 85dbe56..b01ef9a 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,4 +1,4 @@ export * from "./task"; export * from "./model/Model"; -export * from "./model/InMemoryStorage"; +export * from "./model/ModelRepository"; export * from "./provider/ProviderRegistry"; diff --git a/packages/ai/src/model/Model.ts b/packages/ai/src/model/Model.ts index 3baa9be..fba8510 100644 --- a/packages/ai/src/model/Model.ts +++ b/packages/ai/src/model/Model.ts @@ -5,7 +5,7 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -export enum ModelProcessorEnum { +export enum ModelProviderEnum { LOCAL_ONNX_TRANSFORMERJS = "LOCAL_ONNX_TRANSFORMERJS", MEDIA_PIPE_TFJS_MODEL = "MEDIA_PIPE_TFJS_MODEL", LOCAL_MLC = "LOCAL_MLC", @@ -25,38 +25,98 @@ export enum ModelUseCaseEnum { TEXT_TRANSLATION = "TEXT_TRANSLATION", } -const runningOnServer = typeof (globalThis as any).window === "undefined"; +export interface ModelPrimaryKey { + name: string; + provider: ModelProviderEnum; + quantization: string; +} + +export const ModelPrimaryKeySchema = { + name: "string", + provider: "string", + quantization: "string", +} as const; -export interface ModelOptions { - nativeDimensions?: number; // Matryoshka Representation Learning (MRL) -- can truncate embedding dimensions from native number - dimensions?: number; - contextWindow?: number; - extras?: Record; - browserOnly?: boolean; - parameters?: number; - languageStyle?: string; +export interface ModelDetail { + useCase: ModelUseCaseEnum; + pipeline: string; + nativeDimensions: number; + usingDimensions: number; + contextWindow: number; + availableOnBrowser: boolean; + availableOnServer: boolean; + parameters: number; + languageStyle: string; } -export abstract class Model implements ModelOptions { - public static readonly all: ModelList = []; - public dimensions?: number; - public nativeDimensions?: number; - public contextWindow?: number; - public normalize: boolean = true; - public browserOnly: boolean = false; - public extras: Record = {}; - public parameters?: number; +export const ModelDetailSchema = { + useCase: "string", + pipeline: "string", + nativeDimensions: "number", + usingDimensions: "number", + contextWindow: "number", + availableOnBrowser: "boolean", + availableOnServer: "boolean", + parameters: "number", + languageStyle: "string", +} as const; + +export class Model implements ModelPrimaryKey, ModelDetail { constructor( public name: string, - public useCase: ModelUseCaseEnum[] = [], - options?: ModelOptions + public provider: ModelProviderEnum, + public quantization: string, + details: ModelDetail ) { - Object.assign(this, options); - if (!(runningOnServer && this.browserOnly)) { - Model.all.push(this); - } + this.useCase = details.useCase; + this.pipeline = details.pipeline; + this.nativeDimensions = details.nativeDimensions; + this.usingDimensions = details.usingDimensions; + this.contextWindow = details.contextWindow; + this.availableOnBrowser = details.availableOnBrowser; + this.availableOnServer = details.availableOnServer; + this.parameters = details.parameters; + this.languageStyle = details.languageStyle; } - abstract readonly type: ModelProcessorEnum; + public useCase: ModelUseCaseEnum; + public pipeline: string; + public nativeDimensions: number; + public usingDimensions: number; + public contextWindow: number; + public availableOnBrowser: boolean; + public availableOnServer: boolean; + public parameters: number; + public languageStyle: string; } -export type ModelList = Model[]; +// const runningOnServer = typeof (globalThis as any).window === "undefined"; + +// export interface ModelOptions { +// nativeDimensions?: number; // Matryoshka Representation Learning (MRL) -- can truncate embedding dimensions from native number +// dimensions?: number; +// contextWindow?: number; +// extras?: Record; +// browserOnly?: boolean; +// parameters?: number; +// languageStyle?: string; +// } + +// export abstract class Model implements ModelOptions { +// public dimensions?: number; +// public nativeDimensions?: number; +// public contextWindow?: number; +// public normalize: boolean = true; +// public browserOnly: boolean = false; +// public extras: Record = {}; +// public parameters?: number; +// constructor( +// public name: string, +// public useCase: ModelUseCaseEnum[] = [], +// options?: ModelOptions +// ) { +// Object.assign(this, options); +// } +// abstract readonly type: ModelProviderEnum; +// } + +// export type ModelList = Model[]; diff --git a/packages/ai/src/model/ModelRepository.ts b/packages/ai/src/model/ModelRepository.ts new file mode 100644 index 0000000..ac10135 --- /dev/null +++ b/packages/ai/src/model/ModelRepository.ts @@ -0,0 +1,51 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import EventEmitter from "eventemitter3"; +import { KVRepository } from "ellmers-core"; +import { + Model, + ModelPrimaryKey, + ModelProviderEnum, + ModelUseCaseEnum, + ModelPrimaryKeySchema, +} from "./Model"; + +export type ModelEvents = "models_cleared"; + +export abstract class ModelRepository { + public type = "TaskOutputRepository"; + abstract kvRepository: KVRepository; + private events = new EventEmitter(); + on(name: ModelEvents, fn: (...args: any[]) => void) { + this.events.on.call(this.events, name, fn); + } + off(name: ModelEvents, fn: (...args: any[]) => void) { + this.events.off.call(this.events, name, fn); + } + emit(name: ModelEvents, ...args: any[]) { + this.events.emit.call(this.events, name, ...args); + } + + findByName(key: unknown) { + if (typeof key != "string") return undefined; + return this.kvRepository.getKeyValue(key); + } + + findByUseCase(usecase: ModelUseCaseEnum) { + return this.kvRepository.getKeyValue({ useCase: usecase.toLowerCase() }); + } + + async clear(): Promise { + await this.kvRepository.deleteAll(); + this.emit("models_cleared"); + } + + async size(): Promise { + return await this.kvRepository.size(); + } +} diff --git a/packages/ai/src/provider/ProviderRegistry.ts b/packages/ai/src/provider/ProviderRegistry.ts index 3347c84..f7f57e0 100644 --- a/packages/ai/src/provider/ProviderRegistry.ts +++ b/packages/ai/src/provider/ProviderRegistry.ts @@ -5,7 +5,7 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import type { ModelProcessorEnum } from "../model/Model"; +import type { ModelProviderEnum } from "../model/Model"; import { Job, type JobQueue, @@ -41,14 +41,14 @@ export class ProviderRegistry { {}; registerRunFn( taskType: string, - modelType: ModelProcessorEnum, + modelType: ModelProviderEnum, runFn: (task: any, runInputData: any) => Promise ) { if (!this.runFnRegistry[taskType]) this.runFnRegistry[taskType] = {}; this.runFnRegistry[taskType][modelType] = runFn; } - jobAsRunFn(runtype: string, modelType: ModelProcessorEnum) { + jobAsRunFn(runtype: string, modelType: ModelProviderEnum) { const fn = this.runFnRegistry[runtype]?.[modelType]; return async (task: JobQueueTask, input: Input) => { const queue = this.queues.get(modelType)!; @@ -69,16 +69,16 @@ export class ProviderRegistry { }; } - getDirectRunFn(taskType: string, modelType: ModelProcessorEnum) { + getDirectRunFn(taskType: string, modelType: ModelProviderEnum) { return this.runFnRegistry[taskType]?.[modelType]; } - queues: Map> = new Map(); - registerQueue(modelType: ModelProcessorEnum, jobQueue: JobQueue) { + queues: Map> = new Map(); + registerQueue(modelType: ModelProviderEnum, jobQueue: JobQueue) { this.queues.set(modelType, jobQueue); } - getQueue(modelType: ModelProcessorEnum) { + getQueue(modelType: ModelProviderEnum) { return this.queues.get(modelType); } diff --git a/packages/storage/package.json b/packages/storage/package.json index d95a99e..d9051b2 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -5,8 +5,8 @@ "description": "Ellmers is a tool for building and running DAG pipelines of AI tasks.", "scripts": { "watch": "concurrently -c 'auto' 'bun:watch-*'", - "watch-browser": "bun build --watch --no-clear-screen --target=browser --sourcemap=external --external ellmers-core --outdir ./dist/browser ./src/browser/*/index.ts", - "watch-node": "bun build --watch --no-clear-screen --target=node --sourcemap=external --external ellmers-core --outdir ./dist ./src/node/*/index.ts", + "watch-browser": "bun build --watch --no-clear-screen --target=browser --sourcemap=external --external ellmers-core --external ellmers-ai --outdir ./dist/browser ./src/browser/*/index.ts", + "watch-node": "bun build --watch --no-clear-screen --target=node --sourcemap=external --external ellmers-core --external ellmers-ai --outdir ./dist ./src/node/*/index.ts", "watch-bun": "bun build --watch --no-clear-screen --target=bun --sourcemap=external --external ellmers-core --outdir ./dist ./src/bun/*/index.ts", "watch-types": "tsc --watch --preserveWatchOutput", "build": "bun run build-clean && bun run build-types && bun run build-browser && bun run build-node && bun run build-bun", @@ -48,6 +48,7 @@ "dist" ], "dependencies": { - "ellmers-core": "workspace:packages/core" + "ellmers-core": "workspace:packages/core", + "ellmers-ai": "workspace:packages/ai" } } diff --git a/packages/ai/src/model/InMemoryStorage.ts b/packages/storage/src/browser/inmemory/InMemoryModelRepository.ts similarity index 50% rename from packages/ai/src/model/InMemoryStorage.ts rename to packages/storage/src/browser/inmemory/InMemoryModelRepository.ts index d0f87ed..70a3990 100644 --- a/packages/ai/src/model/InMemoryStorage.ts +++ b/packages/storage/src/browser/inmemory/InMemoryModelRepository.ts @@ -5,17 +5,15 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { Model, ModelUseCaseEnum } from "./Model"; +import { Model, ModelRepository } from "ellmers-ai"; +import { InMemoryKVRepository } from "./base/InMemoryKVRepository"; +import { ModelPrimaryKeySchema } from "ellmers-ai"; -export function findModelByName(name: string) { - if (typeof name != "string") return undefined; - return Model.all.find((m) => m.name.toLowerCase() == name.toLowerCase()); -} - -export function findModelByUseCase(usecase: ModelUseCaseEnum) { - return Model.all.filter((m) => m.useCase.includes(usecase)); -} - -export function findAllModels() { - return Model.all.slice(); +export class InMemoryModelRepository extends ModelRepository { + kvRepository: InMemoryKVRepository; + public type = "InMemoryModelRepository" as const; + constructor() { + super(); + this.kvRepository = new InMemoryKVRepository(); + } } diff --git a/packages/storage/src/bun/sqlite/SqliteModelRepository.ts b/packages/storage/src/bun/sqlite/SqliteModelRepository.ts new file mode 100644 index 0000000..98b842d --- /dev/null +++ b/packages/storage/src/bun/sqlite/SqliteModelRepository.ts @@ -0,0 +1,22 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { Model, ModelRepository, ModelPrimaryKeySchema } from "ellmers-ai"; +import { SqliteKVRepository } from "./base/SqliteKVRepository"; + +export class SqliteModelRepository extends ModelRepository { + kvRepository: SqliteKVRepository; + public type = "SqliteModelRepository" as const; + constructor(dbOrPath: string) { + super(); + this.kvRepository = new SqliteKVRepository( + dbOrPath, + "aimodel", + ModelPrimaryKeySchema + ); + } +} diff --git a/packages/storage/tsconfig.json b/packages/storage/tsconfig.json index 69bf5c5..2e09cad 100644 --- a/packages/storage/tsconfig.json +++ b/packages/storage/tsconfig.json @@ -18,5 +18,5 @@ "ellmers-core": ["../core/src"] } }, - "references": [{ "path": "../core" }] + "references": [{ "path": "../core" }, { "path": "../ai" }] } From 7f8f9e4922abeb7c90f174786513d3c9d7f6e901 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Wed, 15 Jan 2025 21:43:29 -0800 Subject: [PATCH 05/12] feat: add table and schema validation utility for KVRepositories - Introduced `validateTableAndSchema` function in `common_sql_helpers.ts` to enforce naming conventions and prevent schema conflicts. - Integrated validation in `SqliteKVRepository` and `PostgresKVRepository` constructors to ensure proper table and schema setup during initialization. --- .../src/bun/sqlite/base/SqliteKVRepository.ts | 4 +- .../postgres/base/PostgresKVRepository.ts | 2 + .../storage/src/util/common_sql_helpers.ts | 41 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 packages/storage/src/util/common_sql_helpers.ts diff --git a/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts b/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts index 47257eb..b3abb44 100644 --- a/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts +++ b/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts @@ -17,8 +17,7 @@ import { KVRepository, BasicValueType, } from "ellmers-core"; - -type SQLiteValueTypes = string | number | boolean | null; +import { validateTableAndSchema } from "../../../util/common_sql_helpers"; // SqliteKVRepository is a key-value store that uses SQLite as the backend for // in app data. @@ -43,6 +42,7 @@ export class SqliteKVRepository< } else { this.db = dbOrPath; } + validateTableAndSchema(this.table, this.primaryKeySchema, this.valueSchema); this.setupDatabase(); } diff --git a/packages/storage/src/node/postgres/base/PostgresKVRepository.ts b/packages/storage/src/node/postgres/base/PostgresKVRepository.ts index 6baa1d6..2cc51fa 100644 --- a/packages/storage/src/node/postgres/base/PostgresKVRepository.ts +++ b/packages/storage/src/node/postgres/base/PostgresKVRepository.ts @@ -17,6 +17,7 @@ import { DefaultValueType, KVRepository, } from "ellmers-core"; +import { validateTableAndSchema } from "../../../util/common_sql_helpers"; // PostgresKVRepository is a key-value store that uses PostgreSQL as the backend for // multi-user scenarios. It supports discriminators. @@ -38,6 +39,7 @@ export class PostgresKVRepository< ) { super(primaryKeySchema, valueSchema); this.pool = new Pool({ connectionString }); + validateTableAndSchema(this.table, this.primaryKeySchema, this.valueSchema); this.setupDatabase(); } diff --git a/packages/storage/src/util/common_sql_helpers.ts b/packages/storage/src/util/common_sql_helpers.ts new file mode 100644 index 0000000..3ab1024 --- /dev/null +++ b/packages/storage/src/util/common_sql_helpers.ts @@ -0,0 +1,41 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +export function validateTableAndSchema( + table: string, + primaryKeySchema: Record, + valueSchema: Record +): void { + // check for dumb things + // Check for invalid characters in table name + if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(table)) { + throw new Error( + `Invalid table name: ${table}. Must start with letter/underscore and contain only alphanumeric/underscore characters` + ); + } + + // Check for invalid characters in schema keys + const validateSchemaKeys = (schema: Record) => { + Object.keys(schema).forEach((key) => { + if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(key)) { + throw new Error( + `Invalid schema key: ${key}. Must start with letter/underscore and contain only alphanumeric/underscore characters` + ); + } + }); + }; + validateSchemaKeys(primaryKeySchema); + validateSchemaKeys(valueSchema); + + // Check for duplicate keys between schemas + const primaryKeys = new Set(Object.keys(primaryKeySchema)); + const valueKeys = Object.keys(valueSchema); + const duplicates = valueKeys.filter((key) => primaryKeys.has(key)); + if (duplicates.length > 0) { + throw new Error(`Duplicate keys found in schemas: ${duplicates.join(", ")}`); + } +} From a124b09f6d7dfa484108014ea8308cc6ecf4a6c8 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Thu, 16 Jan 2025 20:12:09 -0800 Subject: [PATCH 06/12] feat: enhance KVRepository with search functionality and improve event handling - Added a `search` method to `KVRepository` and its implementations, allowing for partial key lookups. - Updated constructors to accept a `searchable` parameter, enabling dynamic configuration of searchable fields. - Enhanced documentation across various repository classes to clarify usage and parameters. - Refactored event emission for put, get, delete, and clearall operations to improve consistency and usability. - Removed the `common_sql_helpers.ts` file and integrated its validation logic into a new `BaseSqlKVRepository` class for better code organization. --- .../core/src/storage/base/KVRepository.ts | 120 +++++++++++- .../indexeddb/base/IndexedDbKVRepository.ts | 61 +++++- .../inmemory/base/InMemoryKVRepository.ts | 75 ++++++- .../test/InMemoryKVRepository.test.ts | 4 +- .../src/bun/sqlite/base/SqliteKVRepository.ts | 172 +++++++++------- .../node/filesystem/base/FileKVRepository.ts | 63 +++++- .../filesystem/test/FileKVRepository.test.ts | 4 +- .../postgres/base/PostgresKVRepository.ts | 184 +++++++++++------- .../src/util/base/BaseSqlKVRepository.ts | 183 +++++++++++++++++ .../storage/src/util/common_sql_helpers.ts | 41 ---- 10 files changed, 697 insertions(+), 210 deletions(-) create mode 100644 packages/storage/src/util/base/BaseSqlKVRepository.ts delete mode 100644 packages/storage/src/util/common_sql_helpers.ts diff --git a/packages/core/src/storage/base/KVRepository.ts b/packages/core/src/storage/base/KVRepository.ts index e373251..84631f9 100644 --- a/packages/core/src/storage/base/KVRepository.ts +++ b/packages/core/src/storage/base/KVRepository.ts @@ -8,19 +8,40 @@ import EventEmitter from "eventemitter3"; import { makeFingerprint } from "../../util/Misc"; -export type KVEvents = "put" | "get" | "delete" | "clearall"; +/** + * Type definitions for key-value repository events + */ +export type KVEvents = "put" | "get" | "search" | "delete" | "clearall"; + +/** + * Schema definitions for primary keys and values + */ export type BasicKeyType = string | number | bigint; export type BasicValueType = string | number | bigint | boolean | null; - export type BasePrimaryKeySchema = Record; export type BaseValueSchema = Record; +/** + * Default schema types for simple string key-value pairs + */ export type DefaultPrimaryKeyType = { "kv-key": string }; export const DefaultPrimaryKeySchema: BasePrimaryKeySchema = { "kv-key": "string" } as const; export type DefaultValueType = { "kv-value": string }; export const DefaultValueSchema: BaseValueSchema = { "kv-value": "string" } as const; +/** + * Abstract base class for key-value storage repositories. + * Provides a flexible interface for storing and retrieving data with typed + * keys and values, and supports comound keys and partial key lookup. + * Has a basic event emitter for listening to repository events. + * + * @typeParam Key - Type for the primary key structure + * @typeParam Value - Type for the value structure + * @typeParam PrimaryKeySchema - Schema definition for the primary key + * @typeParam ValueSchema - Schema definition for the value + * @typeParam Combined - Combined type of Key & Value + */ export abstract class KVRepository< Key extends Record = DefaultPrimaryKeyType, Value extends Record = DefaultValueType, @@ -48,30 +69,62 @@ export abstract class KVRepository< ) { this.events.emit.call(this.events, name, ...args); } - + /** + * Indexes for primary key and value columns which are _only_ populated if the + * key or value schema has a single field. + */ protected primaryKeyIndex: string | undefined = undefined; protected valueIndex: string | undefined = undefined; + /** + * Creates a new KVRepository instance + * @param primaryKeySchema - Schema defining the structure of primary keys + * @param valueSchema - Schema defining the structure of values + * @param searchable - Array of columns to make searchable + */ constructor( protected primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, - protected valueSchema: ValueSchema = DefaultValueSchema as ValueSchema + protected valueSchema: ValueSchema = DefaultValueSchema as ValueSchema, + protected searchable: Array = [] ) { this.primaryKeySchema = primaryKeySchema; this.valueSchema = valueSchema; if (Object.keys(primaryKeySchema).length === 1) { - this.primaryKeyIndex = String(this.primaryKeyColumns()[0]); + this.primaryKeyIndex = this.primaryKeyColumns()[0] as string; } if (Object.keys(valueSchema).length === 1) { - this.valueIndex = String(this.valueColumns()[0]); + this.valueIndex = this.valueColumns()[0] as string; + } + if (!searchable.includes(this.primaryKeyColumns()[0])) { + searchable.push(this.primaryKeyColumns()[0]); + } + this.searchable = searchable; + + // make sure all the searchable columns are in the primary key schema or value schema + for (const column of this.searchable) { + if (!(column in this.primaryKeySchema) && !(column in this.valueSchema)) { + throw new Error( + `Searchable column ${column as string} is not in the primary key schema or value schema` + ); + } } } - // Abstract methods for KV repository store + /** + * Core abstract methods that must be implemented by concrete repositories + */ abstract putKeyValue(key: Key, value: Value): Promise; abstract getKeyValue(key: Key): Promise; abstract deleteKeyValue(key: Key | Combined): Promise; abstract deleteAll(): Promise; abstract size(): Promise; + /** + * Stores a key-value pair in the repository. + * Automatically converts simple types to structured format if using default schema. + * + * @param key - Primary key (can be simple type if using a single property key like default schema) + * @param value - Value to store (can be simple type if using a single property value like default schema) + */ public put(key: BasicKeyType | Key, value: Value | BasicValueType): Promise { if (typeof key !== "object" && this.primaryKeyIndex) { key = { [this.primaryKeyIndex]: key } as Key; @@ -82,24 +135,58 @@ export abstract class KVRepository< return this.putKeyValue(key as Key, value as Value); } + /** + * Retrieves a value by its key. + * For default schema, returns the simple value type directly. + * + * @param key - Primary key to look up (can be simple type if using a single property key like default schema) + * @returns The stored value or undefined if not found + */ public async get(key: BasicKeyType | Key): Promise { - if (typeof key !== "object" && this.primaryKeyIndex) { - key = { [this.primaryKeyIndex]: key } as Key; + /* if the key is not an object, and there is a primary key index, then we need to convert the key to an object + * this allows us to do simple "key" / "value" situations without having to use objects like a compound key + * would require */ + const isKeySimple = !!(typeof key !== "object" && this.primaryKeyIndex); + if (isKeySimple) { + key = { [this.primaryKeyIndex!]: key } as Key; } const value = await this.getKeyValue(key as Key); if (typeof value !== "object") return value; - if (this.primaryKeyIndex && this.valueIndex) { + if (isKeySimple && this.valueIndex) { + /* if it looks like we are doing a simple "key" / "value" situation, then we need to return + the value as a simple type as well. */ return value[this.valueIndex] as BasicValueType; } return value as Value; } + /** + * Abstract method to be implemented by concrete repositories to search for key-value pairs + * based on a partial key. + * + * @param key - Partial key to search for + * @returns Promise resolving to an array of combined key-value objects or undefined if not found + */ + public abstract search(key: Partial): Promise; + + /** + * Retrieves both key and value as a combined object. + * + * @param key - Primary key to look up (can be simple type if using a single property key like default schema) + * @returns Combined key-value object or undefined if not found + */ public async getCombined(key: Key): Promise { const value = await this.getKeyValue(key); if (typeof value !== "object") return undefined; return Object.assign({}, key, value) as Combined; } + /** + * Deletes a key-value pair from the repository. + * Automatically converts simple types to structured format if using default schema. + * + * @param key - Primary key to delete (can be simple type if using a single property key like default schema) + */ public delete(key: Key | BasicKeyType): Promise { if (typeof key !== "object" && this.primaryKeyIndex) { key = { [this.primaryKeyIndex]: key } as Key; @@ -115,6 +202,13 @@ export abstract class KVRepository< return Object.keys(this.valueSchema); } + /** + * Utility method to separate a combined object into its key and value components + * based on the defined schemas. + * + * @param obj - Combined key-value object + * @returns Separated key and value objects + */ protected separateKeyValueFromCombined(obj: Combined): { value: Value; key: Key } { if (obj === null) { console.warn("Key is null"); @@ -138,6 +232,12 @@ export abstract class KVRepository< return { value: value as Value, key: key as Key }; } + /** + * Generates a consistent string identifier for a given key. + * + * @param key - Primary key to convert + * @returns Promise resolving to a string fingerprint of the key + */ protected async getKeyAsIdString(key: Key | BasicKeyType): Promise { if (this.primaryKeyIndex && typeof key === "object") { key = key[this.primaryKeyIndex]; diff --git a/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts b/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts index defcd84..7da6da7 100644 --- a/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts +++ b/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts @@ -21,6 +21,17 @@ import { makeFingerprint } from "../../../util/Misc"; // IndexedDbKVRepository is a key-value store that uses IndexedDB as the backend for // simple browser-based examples with no server-side component. It does not support di +/** + * A key-value repository implementation using IndexedDB for browser-based storage. + * This class provides a simple persistent storage solution for web applications + * without requiring a server component. + * + * @template Key - The type of the primary key object + * @template Value - The type of the value object to be stored + * @template PrimaryKeySchema - Schema definition for the primary key + * @template ValueSchema - Schema definition for the value + * @template Combined - Combined type of Key & Value + */ export class IndexedDbKVRepository< Key extends Record = DefaultPrimaryKeyType, Value extends Record = DefaultValueType, @@ -28,15 +39,34 @@ export class IndexedDbKVRepository< ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, Combined extends Key & Value = Key & Value > extends KVRepository { + /** Promise that resolves to the IndexedDB database instance */ private dbPromise: Promise; - constructor(public table: string = "kv_store") { - super(); + /** + * Creates a new IndexedDB-based key-value repository + * @param table - Name of the IndexedDB store to use + * @param primaryKeySchema - Schema defining the structure of primary keys + * @param valueSchema - Schema defining the structure of values + * @param searchable - Array of properties that can be searched (Note: search not implemented) + */ + constructor( + public table: string = "kv_store", + primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, + valueSchema: ValueSchema = DefaultValueSchema as ValueSchema, + protected searchable: Array = [] + ) { + super(primaryKeySchema, valueSchema, searchable); this.dbPromise = ensureIndexedDbTable(this.table, (db) => { db.createObjectStore(table, { keyPath: "id" }); }); } + /** + * Stores a key-value pair in the repository + * @param key - The key object + * @param value - The value object to store + * @emits put - Emitted when the value is successfully stored + */ async putKeyValue(key: Key, value: Value): Promise { const id = await makeFingerprint(key); const db = await this.dbPromise; @@ -54,6 +84,12 @@ export class IndexedDbKVRepository< }); } + /** + * Retrieves a value by its key + * @param key - The key object to look up + * @returns The stored value or undefined if not found + * @emits get - Emitted when a value is retrieved + */ async getKeyValue(key: Key): Promise { const id = await makeFingerprint(key); const db = await this.dbPromise; @@ -75,6 +111,19 @@ export class IndexedDbKVRepository< }); } + /** + * Search functionality is not supported in this implementation + * @throws Error indicating search is not supported + */ + async search(key: Partial): Promise { + throw new Error("Search not supported for IndexedDbKVRepository"); + } + + /** + * Deletes a key-value pair from the repository + * @param key - The key object to delete + * @emits delete - Emitted when a value is deleted + */ async deleteKeyValue(key: Key): Promise { const id = await makeFingerprint(key); const db = await this.dbPromise; @@ -92,6 +141,10 @@ export class IndexedDbKVRepository< }); } + /** + * Deletes all key-value pairs from the repository + * @emits clearall - Emitted when all values are deleted + */ async deleteAll(): Promise { const db = await this.dbPromise; @@ -108,6 +161,10 @@ export class IndexedDbKVRepository< }); } + /** + * Returns the total number of key-value pairs in the repository + * @returns The count of stored items + */ async size(): Promise { const db = await this.dbPromise; diff --git a/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts b/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts index cb8f7c4..2775469 100644 --- a/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts +++ b/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts @@ -8,7 +8,6 @@ import { BaseValueSchema, BasePrimaryKeySchema, - BasicValueType, BasicKeyType, DefaultValueType, DefaultValueSchema, @@ -20,6 +19,16 @@ import { makeFingerprint } from "../../../util/Misc"; // InMemoryKVRepository is a simple in-memory key-value store that can be used for testing or as a cache +/** + * A generic in-memory key-value repository implementation. + * Provides a simple, non-persistent storage solution suitable for testing and caching scenarios. + * + * @template Key - The type of the primary key object, must be a record of basic types + * @template Value - The type of the value object being stored + * @template PrimaryKeySchema - Schema definition for the primary key + * @template ValueSchema - Schema definition for the value + * @template Combined - The combined type of Key & Value + */ export class InMemoryKVRepository< Key extends Record = DefaultPrimaryKeyType, Value extends Record = DefaultValueType, @@ -27,39 +36,93 @@ export class InMemoryKVRepository< ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, Combined extends Key & Value = Key & Value > extends KVRepository { - values = new Map(); + /** Internal storage using a Map with fingerprint strings as keys */ + values = new Map(); + /** + * Creates a new InMemoryKVRepository instance + * @param primaryKeySchema - Schema defining the structure of primary keys + * @param valueSchema - Schema defining the structure of values + * @param searchable - Array of field names that can be searched + */ constructor( primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, - valueSchema: ValueSchema = DefaultValueSchema as ValueSchema + valueSchema: ValueSchema = DefaultValueSchema as ValueSchema, + searchable: Array = [] ) { - super(primaryKeySchema, valueSchema); + super(primaryKeySchema, valueSchema, searchable); } + /** + * Stores a key-value pair in the repository + * @param key - The primary key object + * @param value - The value object to store + * @emits 'put' event with the fingerprint ID when successful + */ async putKeyValue(key: Key, value: Value): Promise { const id = await makeFingerprint(key); - this.values.set(id, value); + this.values.set(id, Object.assign({}, key, value) as Combined); this.emit("put", id); } + /** + * Retrieves a value by its key + * @param key - The primary key object to look up + * @returns The value object if found, undefined otherwise + * @emits 'get' event with the fingerprint ID and value when found + */ async getKeyValue(key: Key): Promise { const id = await makeFingerprint(key); const out = this.values.get(id); + if (out === undefined) { + return undefined; + } this.emit("get", id, out); - return out; + const { value } = this.separateKeyValueFromCombined(out); + return value; } + /** + * Searches for entries matching a partial key-value pair + * @param key - Partial combined key-value object to search for + * @returns Array of matching combined objects + * @throws Error if search criteria contains more than one key + */ + async search(key: Partial): Promise { + const search = Object.keys(key); + if (search.length !== 1) { + throw new Error("Search must be a single key"); + } + this.emit("search", key); + return Array.from(this.values.entries()) + .filter(([_fingerprint, value]) => value[search[0]] === key[search[0]]) + .map(([_id, value]) => value); + } + + /** + * Deletes an entry by its key + * @param key - The primary key object of the entry to delete + * @emits 'delete' event with the fingerprint ID when successful + */ async deleteKeyValue(key: Key): Promise { const id = await makeFingerprint(key); this.values.delete(id); this.emit("delete", id); } + /** + * Removes all entries from the repository + * @emits 'clearall' event when successful + */ async deleteAll(): Promise { this.values.clear(); this.emit("clearall"); } + /** + * Returns the number of entries in the repository + * @returns The total count of stored key-value pairs + */ async size(): Promise { return this.values.size; } diff --git a/packages/storage/src/browser/inmemory/test/InMemoryKVRepository.test.ts b/packages/storage/src/browser/inmemory/test/InMemoryKVRepository.test.ts index a9c6502..debfe92 100644 --- a/packages/storage/src/browser/inmemory/test/InMemoryKVRepository.test.ts +++ b/packages/storage/src/browser/inmemory/test/InMemoryKVRepository.test.ts @@ -30,8 +30,8 @@ describe("InMemoryKVRepository", () => { }); it("should store and retrieve values for a key", async () => { - const key = "key"; - const value = "value"; + const key = "key1"; + const value = "value1"; await repository.put(key, value); const output = await repository.get(key); diff --git a/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts b/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts index b3abb44..dd65432 100644 --- a/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts +++ b/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts @@ -14,38 +14,57 @@ import { DefaultValueSchema, DefaultPrimaryKeyType, DefaultPrimaryKeySchema, - KVRepository, - BasicValueType, } from "ellmers-core"; -import { validateTableAndSchema } from "../../../util/common_sql_helpers"; +import { BaseSqlKVRepository } from "../../../util/base/BaseSqlKVRepository"; // SqliteKVRepository is a key-value store that uses SQLite as the backend for // in app data. +/** + * A SQLite-based key-value repository implementation. + * @template Key - The type of the primary key object, must be a record of basic types + * @template Value - The type of the value object being stored + * @template PrimaryKeySchema - Schema definition for the primary key + * @template ValueSchema - Schema definition for the value + * @template Combined - Combined type of Key & Value + */ export class SqliteKVRepository< Key extends Record = DefaultPrimaryKeyType, Value extends Record = DefaultValueType, PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, Combined extends Key & Value = Key & Value -> extends KVRepository { +> extends BaseSqlKVRepository { + /** The SQLite database instance */ private db: Database; + + /** + * Creates a new SQLite key-value repository + * @param dbOrPath - Either a Database instance or a path to the SQLite database file + * @param table - The name of the table to use for storage (defaults to 'kv_store') + * @param primaryKeySchema - Schema defining the structure of the primary key + * @param valueSchema - Schema defining the structure of the values + * @param searchable - Array of columns to make searchable + */ constructor( dbOrPath: string, - public table: string = "kv_store", + table: string = "kv_store", primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, - valueSchema: ValueSchema = DefaultValueSchema as ValueSchema + valueSchema: ValueSchema = DefaultValueSchema as ValueSchema, + searchable: Array = [] ) { - super(primaryKeySchema, valueSchema); + super(table, primaryKeySchema, valueSchema, searchable); if (typeof dbOrPath === "string") { this.db = new Database(dbOrPath); } else { this.db = dbOrPath; } - validateTableAndSchema(this.table, this.primaryKeySchema, this.valueSchema); this.setupDatabase(); } + /** + * Creates the database table if it doesn't exist with the defined schema + */ public setupDatabase(): void { const sql = ` CREATE TABLE IF NOT EXISTS \`${this.table}\` ( @@ -55,37 +74,24 @@ export class SqliteKVRepository< ) `; this.db.exec(sql); + for (const column of this.searchable) { + /* Makes other columns searchable, but excludes the first column + of a primary key (which would be redundant) */ + if (column !== this.primaryKeyColumns()[0]) { + this.db.exec( + `CREATE INDEX IF NOT EXISTS \`${this.table}_${column as string}\` + ON \`${this.table}\` (\`${column as string}\`)` + ); + } + } } - private constructPrimaryKeyColumns(): string { - const cols = Object.entries(this.primaryKeySchema) - .map(([key, type]) => { - // Convert the provided type to a SQL type, assuming simple mappings; adjust as necessary - const sqlType = this.mapTypeToSQL(type); - return `\`${key}\` ${sqlType} NOT NULL`; - }) - .join(", "); - return cols; - } - - private constructValueColumns(): string { - const cols = Object.entries(this.valueSchema) - .map(([key, type]) => { - const sqlType = this.mapTypeToSQL(type); - return `\`${key}\` ${sqlType} NULL`; - }) - .join(", "); - return cols; - } - - protected primaryKeyColumnList(): string { - return "`" + this.primaryKeyColumns().join("`, `") + "`"; - } - protected valueColumnList(): string { - return "`" + this.valueColumns().join("`, `") + "`"; - } - - private mapTypeToSQL(type: string): string { + /** + * Maps TypeScript/JavaScript types to their SQLite column type equivalents + * @param type - The TypeScript/JavaScript type to map + * @returns The corresponding SQLite column type + */ + protected mapTypeToSQL(type: string): string { // Basic type mapping; extend according to your needs switch (type) { case "string": @@ -98,43 +104,17 @@ export class SqliteKVRepository< } } - // JS objects are not ordered, so we need to convert them to an ordered array - // so that we can use them as parameters in a SQL query - // we will order base on the valueSchema - getValueAsOrderedArray(value: Value): BasicValueType[] { - const orderedParams: BasicValueType[] = []; - // Iterate through valueSchema to maintain consistent order - for (const [key, type] of Object.entries(this.valueSchema)) { - if (key in value) { - orderedParams.push(value[key]); - } else { - throw new Error(`Missing required value field: ${key}`); - } - } - return orderedParams; - } - - // JS objects are not ordered, so we need to convert them to an ordered array - // so that we can use them as parameters in a SQL query - // we will order base on the primaryKeySchema - getPrimaryKeyAsOrderedArray(key: Key): BasicKeyType[] { - const orderedParams: BasicKeyType[] = []; - // Iterate through primaryKeySchema to maintain consistent order - for (const [k, type] of Object.entries(this.primaryKeySchema)) { - if (k in key) { - orderedParams.push(key[k]); - } else { - throw new Error(`Missing required primary key field: ${k}`); - } - } - return orderedParams; - } - + /** + * Stores a key-value pair in the database + * @param key - The primary key object + * @param value - The value object to store + * @emits 'put' event when successful + */ async putKeyValue(key: Key, value: Value): Promise { const sql = ` - INSERT OR REPLACE INTO ${ + INSERT OR REPLACE INTO \`${ this.table - } (${this.primaryKeyColumnList()}, ${this.valueColumnList()}) + }\` (${this.primaryKeyColumnList()}, ${this.valueColumnList()}) VALUES ( ${this.primaryKeyColumns().map((i) => "?")}, ${this.valueColumns().map((i) => "?")} @@ -151,15 +131,20 @@ export class SqliteKVRepository< this.emit("put", key); } + /** + * Retrieves a value from the database by its key + * @param key - The primary key object to look up + * @returns The stored value or undefined if not found + * @emits 'get' event when successful + */ async getKeyValue(key: Key): Promise { const whereClauses = (this.primaryKeyColumns() as string[]) .map((key) => `\`${key}\` = ?`) .join(" AND "); const sql = ` - SELECT ${this.valueColumnList()} FROM ${this.table} WHERE ${whereClauses} + SELECT ${this.valueColumnList()} FROM \`${this.table}\` WHERE ${whereClauses} `; - // const sql = `SELECT * FROM ${this.table} `; const stmt = this.db.prepare(sql); const params = this.getPrimaryKeyAsOrderedArray(key); const value = stmt.get(...params); @@ -171,6 +156,39 @@ export class SqliteKVRepository< } } + /** + * Method to be implemented by concrete repositories to search for key-value pairs + * based on a partial key. + * + * @param key - Partial key to search for + * @returns Promise resolving to an array of combined key-value objects or undefined if not found + */ + public async search(key: Partial): Promise { + const search = Object.keys(key); + if (search.length !== 1) { + //TODO: make this work with any prefix of primary key + throw new Error("Search must be a single key"); + } + + const sql = ` + SELECT * FROM \`${this.table}\` + WHERE \`${search[0]}\` = ? + `; + const stmt = this.db.prepare(sql); + const value = stmt.all(key[search[0]]); + if (value) { + this.emit("search"); + return value; + } else { + return undefined; + } + } + + /** + * Deletes a key-value pair from the database + * @param key - The primary key object to delete + * @emits 'delete' event when successful + */ async deleteKeyValue(key: Key): Promise { const whereClauses = (this.primaryKeyColumns() as string[]) .map((key) => `${key} = ?`) @@ -181,11 +199,19 @@ export class SqliteKVRepository< this.emit("delete", key); } + /** + * Deletes all entries from the database table + * @emits 'clearall' event when successful + */ async deleteAll(): Promise { this.db.exec(`DELETE FROM ${this.table}`); this.emit("clearall"); } + /** + * Gets the total number of entries in the database table + * @returns The count of entries + */ async size(): Promise { const stmt = this.db.prepare<{ count: number }, []>(` SELECT COUNT(*) AS count FROM ${this.table} diff --git a/packages/storage/src/node/filesystem/base/FileKVRepository.ts b/packages/storage/src/node/filesystem/base/FileKVRepository.ts index fd7747a..5b7aa8b 100644 --- a/packages/storage/src/node/filesystem/base/FileKVRepository.ts +++ b/packages/storage/src/node/filesystem/base/FileKVRepository.ts @@ -20,9 +20,16 @@ import { KVRepository, } from "ellmers-core"; -// FileKVRepository is a key-value store that uses the file system as the backend for -// simple scenarios. - +/** + * A key-value repository implementation that uses the filesystem for storage. + * Each key-value pair is stored as a separate JSON file in the specified directory. + * + * @template Key - The type of the primary key object, defaults to DefaultPrimaryKeyType + * @template Value - The type of the value object, defaults to DefaultValueType + * @template PrimaryKeySchema - The schema for the primary key, defaults to DefaultPrimaryKeySchema + * @template ValueSchema - The schema for the value, defaults to DefaultValueSchema + * @template Combined - The combined type of Key & Value + */ export class FileKVRepository< Key extends Record = DefaultPrimaryKeyType, Value extends Record = DefaultValueType, @@ -32,16 +39,31 @@ export class FileKVRepository< > extends KVRepository { private folderPath: string; + /** + * Creates a new FileKVRepository instance. + * + * @param folderPath - The directory path where the JSON files will be stored + * @param primaryKeySchema - Schema defining the structure of the primary key + * @param valueSchema - Schema defining the structure of the values + * @param searchable - Array of keys that can be used for searching (Note: search is not supported in this implementation) + */ constructor( folderPath: string, primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, - valueSchema: ValueSchema = DefaultValueSchema as ValueSchema + valueSchema: ValueSchema = DefaultValueSchema as ValueSchema, + searchable: Array = [] ) { - super(primaryKeySchema, valueSchema); + super(primaryKeySchema, valueSchema, searchable); this.folderPath = path.dirname(folderPath); mkdirSync(this.folderPath, { recursive: true }); } + /** + * Stores a key-value pair in the repository + * @param key - The primary key object + * @param value - The value object to store + * @emits 'put' event with the fingerprint ID when successful + */ async putKeyValue(key: Key, value: Value): Promise { const filePath = await this.getFilePath(key); try { @@ -52,6 +74,12 @@ export class FileKVRepository< this.emit("put", key); } + /** + * Retrieves a value by its key + * @param key - The primary key object to look up + * @returns The value object if found, undefined otherwise + * @emits 'get' event with the fingerprint ID and value when found + */ async getKeyValue(key: Key): Promise { const filePath = await this.getFilePath(key); try { @@ -65,6 +93,11 @@ export class FileKVRepository< } } + /** + * Deletes an entry by its key + * @param key - The primary key object of the entry to delete + * @emits 'delete' event with the fingerprint ID when successful + */ async deleteKeyValue(key: Key): Promise { const filePath = await this.getFilePath(key); try { @@ -75,12 +108,20 @@ export class FileKVRepository< this.emit("delete", key); } + /** + * Removes all entries from the repository + * @emits 'clearall' event when successful + */ async deleteAll(): Promise { // Delete all files in the folder ending in .json await rm(this.folderPath, { recursive: true, force: true }); this.emit("clearall"); } + /** + * Returns the number of entries in the repository + * @returns The total count of stored key-value pairs + */ async size(): Promise { // Count all files in the folder ending in .json const globPattern = path.join(this.folderPath, "*.json"); @@ -88,6 +129,18 @@ export class FileKVRepository< return files.length; } + /** + * Search is not supported in the filesystem implementation. + * @throws {Error} Always throws an error indicating search is not supported + */ + async search(key: Partial): Promise { + throw new Error("Search not supported for FileKVRepository"); + } + + /** + * Generates the full filesystem path for a given key. + * @private + */ private async getFilePath(key: Key | BasicKeyType): Promise { const filename = await this.getKeyAsIdString(key); const fullPath = path.join(this.folderPath, `${filename}.json`); diff --git a/packages/storage/src/node/filesystem/test/FileKVRepository.test.ts b/packages/storage/src/node/filesystem/test/FileKVRepository.test.ts index a9dea4f..36c76d2 100644 --- a/packages/storage/src/node/filesystem/test/FileKVRepository.test.ts +++ b/packages/storage/src/node/filesystem/test/FileKVRepository.test.ts @@ -29,7 +29,7 @@ describe("FileKVRepository", () => { rmdirSync(testDir, { recursive: true }); beforeEach(() => { - repository = new FileKVRepository(testDir, {}); + repository = new FileKVRepository(testDir); }); afterEach(() => { repository.deleteAll(); @@ -67,7 +67,7 @@ describe("FileKVRepository", () => { repository = new FileKVRepository(testDir, PrimaryKeySchema, ValueSchema); }); afterEach(async () => { - await repository.deleteAll(); + // await repository.deleteAll(); }); it("should store and retrieve values for a key", async () => { diff --git a/packages/storage/src/node/postgres/base/PostgresKVRepository.ts b/packages/storage/src/node/postgres/base/PostgresKVRepository.ts index 2cc51fa..a516eed 100644 --- a/packages/storage/src/node/postgres/base/PostgresKVRepository.ts +++ b/packages/storage/src/node/postgres/base/PostgresKVRepository.ts @@ -10,39 +10,68 @@ import { BaseValueSchema, BasePrimaryKeySchema, BasicKeyType, - BasicValueType, DefaultValueSchema, DefaultPrimaryKeySchema, DefaultPrimaryKeyType, DefaultValueType, - KVRepository, } from "ellmers-core"; -import { validateTableAndSchema } from "../../../util/common_sql_helpers"; - -// PostgresKVRepository is a key-value store that uses PostgreSQL as the backend for -// multi-user scenarios. It supports discriminators. - +import { BaseSqlKVRepository } from "../../../util/base/BaseSqlKVRepository"; + +/// ****************************************************************** +/// * +/// ****************************************************************** +/// ********************** NOT TESTED YET *********************** +/// ****************************************************************** +/// * +/// ****************************************************************** +/// really... i wrote it and it passes the linter only! + +/** +/** + * A PostgreSQL-based key-value repository implementation that extends BaseSqlKVRepository. + * This class provides persistent storage for key-value pairs in a PostgreSQL database, + * making it suitable for multi-user scenarios. + * + * @template Key - The type of the primary key, must be a record of basic types + * @template Value - The type of the stored value, can be any record type + * @template PrimaryKeySchema - Schema definition for the primary key + * @template ValueSchema - Schema definition for the value + * @template Combined - Combined type of Key & Value + */ export class PostgresKVRepository< Key extends Record = DefaultPrimaryKeyType, Value extends Record = DefaultValueType, PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, Combined extends Key & Value = Key & Value -> extends KVRepository { +> extends BaseSqlKVRepository { private pool: Pool; + /** + * Creates a new PostgresKVRepository instance. + * + * @param connectionString - PostgreSQL connection string + * @param table - Name of the table to store key-value pairs (defaults to "kv_store") + * @param primaryKeySchema - Schema definition for primary key columns + * @param valueSchema - Schema definition for value columns + * @param searchable - Array of columns to make searchable + */ constructor( connectionString: string, public table: string = "kv_store", primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, - valueSchema: ValueSchema = DefaultValueSchema as ValueSchema + valueSchema: ValueSchema = DefaultValueSchema as ValueSchema, + searchable: Array = [] ) { - super(primaryKeySchema, valueSchema); + super(table, primaryKeySchema, valueSchema, searchable); this.pool = new Pool({ connectionString }); - validateTableAndSchema(this.table, this.primaryKeySchema, this.valueSchema); this.setupDatabase(); } + /** + * Initializes the database table with the required schema. + * Creates the table if it doesn't exist with primary key and value columns. + */ private async setupDatabase(): Promise { await this.pool.query(` CREATE TABLE IF NOT EXISTS \`${this.table}\` ( @@ -51,37 +80,25 @@ export class PostgresKVRepository< PRIMARY KEY (${this.primaryKeyColumnList()}) ) `); + for (const column of this.searchable) { + if (column !== this.primaryKeyColumns()[0]) { + /* Makes other columns searchable, but excludes the first column + of a primary key (which would be redundant) */ + await this.pool.query( + `CREATE INDEX IF NOT EXISTS \`${this.table}_${column as string}\` + ON \`${this.table}\` (\`${column as string}\`)` + ); + } + } } - private constructPrimaryKeyColumns(): string { - const cols = Object.entries(this.primaryKeySchema) - .map(([key, type]) => { - // Convert the provided type to a SQL type, assuming simple mappings; adjust as necessary - const sqlType = this.mapTypeToSQL(type); - return `\`${key}\` ${sqlType} NOT NULL`; - }) - .join(", "); - return cols; - } - - private constructValueColumns(): string { - const cols = Object.entries(this.valueSchema) - .map(([key, type]) => { - const sqlType = this.mapTypeToSQL(type); - return `\`${key}\` ${sqlType} NULL`; - }) - .join(", "); - return cols; - } - - protected primaryKeyColumnList(): string { - return "`" + this.primaryKeyColumns().join("`, `") + "`"; - } - protected valueColumnList(): string { - return "`" + this.valueColumns().join("`, `") + "`"; - } - - private mapTypeToSQL(type: string): string { + /** + * Maps TypeScript/JavaScript types to corresponding PostgreSQL data types. + * + * @param type - The TypeScript/JavaScript type to map + * @returns The corresponding PostgreSQL data type + */ + protected mapTypeToSQL(type: string): string { // Basic type mapping; extend according to your needs switch (type) { case "string": @@ -94,34 +111,14 @@ export class PostgresKVRepository< } } - // JS objects are not ordered, so we need to convert them to an ordered array - // so that we can use them as parameters in a SQL query - // we will order base on the valueSchema - getValueAsOrderedArray(value: Value): BasicValueType[] { - const orderedParams: BasicValueType[] = []; - // Iterate through valueSchema to maintain consistent order - for (const [key, type] of Object.entries(this.valueSchema)) { - orderedParams.push(value[key] ?? null); - } - return orderedParams; - } - - // JS objects are not ordered, so we need to convert them to an ordered array - // so that we can use them as parameters in a SQL query - // we will order base on the primaryKeySchema - getPrimaryKeyAsOrderedArray(key: Key): BasicKeyType[] { - const orderedParams: BasicKeyType[] = []; - // Iterate through primaryKeySchema to maintain consistent order - for (const [k, type] of Object.entries(this.primaryKeySchema)) { - if (k in key) { - orderedParams.push(key[k]); - } else { - throw new Error(`Missing required primary key field: ${k}`); - } - } - return orderedParams; - } - + /** + * Stores or updates a key-value pair in the database. + * Uses UPSERT (INSERT ... ON CONFLICT DO UPDATE) for atomic operations. + * + * @param key - The primary key object + * @param value - The value object to store + * @emits "put" event with the key when successful + */ async putKeyValue(key: Key, value: Value): Promise { const sql = ` INSERT INTO \`${this.table}\` ( @@ -143,6 +140,13 @@ export class PostgresKVRepository< this.emit("put", key); } + /** + * Retrieves a value from the database by its primary key. + * + * @param key - The primary key object to look up + * @returns The stored value or undefined if not found + * @emits "get" event with the key when successful + */ async getKeyValue(key: Key): Promise { const whereClauses = (this.primaryKeyColumns() as string[]) .map((discriminatorKey, i) => `\`${discriminatorKey}\` = $${i + 1}`) @@ -163,6 +167,39 @@ export class PostgresKVRepository< } } + /** + * Method to be implemented by concrete repositories to search for key-value pairs + * based on a partial key. + * + * @param key - Partial key to search for + * @returns Promise resolving to an array of combined key-value objects or undefined if not found + */ + public async search(key: Partial): Promise { + const search = Object.keys(key); + if (search.length !== 1) { + //TODO: make this work with any prefix of primary key + throw new Error("Search must be a single key"); + } + + const sql = ` + SELECT * FROM \`${this.table}\` + WHERE \`${search[0]}\` = ? + `; + const result = await this.pool.query(sql, [key[search[0]]]); + if (result.rows.length > 0) { + this.emit("search"); + return result.rows; + } else { + return undefined; + } + } + + /** + * Deletes a key-value pair from the database. + * + * @param key - The primary key object to delete + * @emits "delete" event with the key when successful + */ async deleteKeyValue(key: Key): Promise { const whereClauses = (this.primaryKeyColumns() as string[]) .map((key, i) => `\`${key}\` = $${i + 1}`) @@ -173,13 +210,22 @@ export class PostgresKVRepository< this.emit("delete", key); } + /** + * Deletes all key-value pairs from the database table. + * @emits "clearall" event when successful + */ async deleteAll(): Promise { await this.pool.query(`DELETE FROM \`${this.table}\``); this.emit("clearall"); } + /** + * Returns the total number of key-value pairs in the database. + * + * @returns Promise resolving to the count of stored items + */ async size(): Promise { - const result = await this.pool.query(`SELECT COUNT(*) FROM ${this.table}`); + const result = await this.pool.query(`SELECT COUNT(*) FROM \`${this.table}\``); return parseInt(result.rows[0].count, 10); } } diff --git a/packages/storage/src/util/base/BaseSqlKVRepository.ts b/packages/storage/src/util/base/BaseSqlKVRepository.ts new file mode 100644 index 0000000..869e43a --- /dev/null +++ b/packages/storage/src/util/base/BaseSqlKVRepository.ts @@ -0,0 +1,183 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { + BaseValueSchema, + BasicKeyType, + BasePrimaryKeySchema, + DefaultValueType, + DefaultValueSchema, + DefaultPrimaryKeyType, + DefaultPrimaryKeySchema, + KVRepository, + BasicValueType, +} from "ellmers-core"; + +// BaseKVRepository is a key-value store that uses SQLite and Postgres use as common code + +/** + * Base class for SQL-based key-value repositories that implements common functionality + * for both SQLite and PostgreSQL database implementations. + * + * @template Key - The type of the primary key object, must be a record of basic types + * @template Value - The type of the value object being stored + * @template PrimaryKeySchema - Schema definition for the primary key + * @template ValueSchema - Schema definition for the value + * @template Combined - Combined type of Key & Value in case just combining them is not enough + */ +export abstract class BaseSqlKVRepository< + Key extends Record = DefaultPrimaryKeyType, + Value extends Record = DefaultValueType, + PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, + ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, + Combined extends Key & Value = Key & Value +> extends KVRepository { + /** + * Creates a new instance of BaseSqlKVRepository + * @param table - The name of the database table to use for storage + * @param primaryKeySchema - Schema defining the structure of the primary key + * @param valueSchema - Schema defining the structure of the stored values + * @param searchable - Array of columns to make searchable + */ + constructor( + public table: string = "kv_store", + primaryKeySchema: PrimaryKeySchema = DefaultPrimaryKeySchema as PrimaryKeySchema, + valueSchema: ValueSchema = DefaultValueSchema as ValueSchema, + protected searchable: Array = [] + ) { + super(primaryKeySchema, valueSchema, searchable); + this.validateTableAndSchema(); + } + + /** + * Maps JavaScript/TypeScript types to their corresponding SQL type + * Must be implemented by derived classes for specific SQL dialects + */ + protected abstract mapTypeToSQL(type: string): string; + + /** + * Generates the SQL column definitions for primary key fields + * @returns SQL string containing primary key column definitions + */ + protected constructPrimaryKeyColumns(): string { + const cols = Object.entries(this.primaryKeySchema) + .map(([key, type]) => { + const sqlType = this.mapTypeToSQL(type); + return `\`${key}\` ${sqlType} NOT NULL`; + }) + .join(", "); + return cols; + } + + /** + * Generates the SQL column definitions for value fields + * @returns SQL string containing value column definitions + */ + protected constructValueColumns(): string { + const cols = Object.entries(this.valueSchema) + .map(([key, type]) => { + const sqlType = this.mapTypeToSQL(type); + return `\`${key}\` ${sqlType} NULL`; + }) + .join(", "); + return cols; + } + + /** + * Returns a comma-separated list of primary key column names + * @returns Formatted string of primary key column names + */ + protected primaryKeyColumnList(): string { + return "`" + this.primaryKeyColumns().join("`, `") + "`"; + } + + /** + * Returns a comma-separated list of value column names + * @returns Formatted string of value column names + */ + protected valueColumnList(): string { + return "`" + this.valueColumns().join("`, `") + "`"; + } + + /** + * Converts a value object into an ordered array based on the valueSchema + * This ensures consistent parameter ordering for SQL queries + * @param value - The value object to convert + * @returns Array of values ordered according to the schema + * @throws Error if a required field is missing + */ + protected getValueAsOrderedArray(value: Value): BasicValueType[] { + const orderedParams: BasicValueType[] = []; + for (const [key, type] of Object.entries(this.valueSchema)) { + if (key in value) { + orderedParams.push(value[key]); + } else { + throw new Error(`Missing required value field: ${key}`); + } + } + return orderedParams; + } + + /** + * Converts a primary key object into an ordered array based on the primaryKeySchema + * This ensures consistent parameter ordering for SQL queries + * @param key - The primary key object to convert + * @returns Array of key values ordered according to the schema + * @throws Error if a required primary key field is missing + */ + protected getPrimaryKeyAsOrderedArray(key: Key): BasicKeyType[] { + const orderedParams: BasicKeyType[] = []; + for (const [k, type] of Object.entries(this.primaryKeySchema)) { + if (k in key) { + orderedParams.push(key[k]); + } else { + throw new Error(`Missing required primary key field: ${k}`); + } + } + return orderedParams; + } + + /** + * Validates table name and schema configurations + * Checks for: + * 1. Valid table name format + * 2. Valid schema key names + * 3. No duplicate keys between primary key and value schemas + * This is a sanity check to make sure the table and schema are valid, + * and to prevent dumb mistakes and mischevious behavior. + * @throws Error if validation fails + */ + protected validateTableAndSchema(): void { + // Check for invalid characters in table name + if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(this.table)) { + throw new Error( + `Invalid table name: ${this.table}. Must start with letter/underscore and contain only alphanumeric/underscore characters` + ); + } + + // Validate schema key naming + const validateSchemaKeys = (schema: Record) => { + Object.keys(schema).forEach((key) => { + if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(key)) { + throw new Error( + `Invalid schema key: ${key}. Must start with letter/underscore and contain only alphanumeric/underscore characters` + ); + } + }); + }; + validateSchemaKeys(this.primaryKeySchema); + validateSchemaKeys(this.valueSchema); + + // Check for key name collisions between schemas + const primaryKeys = new Set(Object.keys(this.primaryKeySchema)); + const valueKeys = Object.keys(this.valueSchema); + const duplicates = valueKeys.filter((key) => primaryKeys.has(key)); + if (duplicates.length > 0) { + throw new Error(`Duplicate keys found in schemas: ${duplicates.join(", ")}`); + } + } +} diff --git a/packages/storage/src/util/common_sql_helpers.ts b/packages/storage/src/util/common_sql_helpers.ts deleted file mode 100644 index 3ab1024..0000000 --- a/packages/storage/src/util/common_sql_helpers.ts +++ /dev/null @@ -1,41 +0,0 @@ -// ******************************************************************************* -// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * -// * * -// * Copyright Steven Roussey * -// * Licensed under the Apache License, Version 2.0 (the "License"); * -// ******************************************************************************* - -export function validateTableAndSchema( - table: string, - primaryKeySchema: Record, - valueSchema: Record -): void { - // check for dumb things - // Check for invalid characters in table name - if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(table)) { - throw new Error( - `Invalid table name: ${table}. Must start with letter/underscore and contain only alphanumeric/underscore characters` - ); - } - - // Check for invalid characters in schema keys - const validateSchemaKeys = (schema: Record) => { - Object.keys(schema).forEach((key) => { - if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(key)) { - throw new Error( - `Invalid schema key: ${key}. Must start with letter/underscore and contain only alphanumeric/underscore characters` - ); - } - }); - }; - validateSchemaKeys(primaryKeySchema); - validateSchemaKeys(valueSchema); - - // Check for duplicate keys between schemas - const primaryKeys = new Set(Object.keys(primaryKeySchema)); - const valueKeys = Object.keys(valueSchema); - const duplicates = valueKeys.filter((key) => primaryKeys.has(key)); - if (duplicates.length > 0) { - throw new Error(`Duplicate keys found in schemas: ${duplicates.join(", ")}`); - } -} From 7251241ea6389f6606fee1ebbc900e65472d2a14 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Fri, 17 Jan 2025 11:22:10 -0800 Subject: [PATCH 07/12] feat: update model handling and task registration for ONNX and MediaPipe WIP -- using a mock registery that is sync only, so not using the storage system even though files are here now, as i need to change the sync part of the task system to either be async and named something else, or pre-download model info. - Changed model identifiers from "Xenova/LaMini-Flan-T5-783M" to "ONNX Xenova/LaMini-Flan-T5-783M q8" across multiple files to standardize model naming. - Introduced a new `ModelRegistry` to manage model instances and their associations with tasks, enhancing the model management system. - Updated task registration functions to utilize the new model registry, ensuring tasks are correctly linked to their respective models. - Refactored various components to improve integration with the new model registry, including adjustments in task execution and model retrieval logic. - Enhanced documentation and type definitions for better clarity and usability. --- docs/developers/01_getting_started.md | 10 +- examples/cli/src/TaskCLI.ts | 34 +- examples/web/package.json | 5 +- examples/web/src/App.tsx | 26 +- examples/web/src/QueueSatus.tsx | 4 +- examples/web/src/main.tsx | 15 +- examples/web/tsconfig.json | 6 +- .../src/ggml/model/GgmlLocalModel.ts | 9 +- .../hf-transformers/bindings/registerTasks.ts | 16 +- .../model/ONNXTransformerJsModel.ts | 23 +- .../provider/HuggingFaceLocal_TaskRun.ts | 42 ++- .../test/HFTransformersBinding.test.ts | 42 ++- .../tf-mediapipe/bindings/registerTasks.ts | 7 +- .../src/tf-mediapipe/model/MediaPipeModel.ts | 14 +- .../provider/MediaPipeLocalTaskRun.ts | 21 +- .../test/TfMediaPipeBinding.test.ts | 180 ++++++----- packages/ai/src/index.ts | 1 + packages/ai/src/model/Model.ts | 123 +------ packages/ai/src/model/ModelRegistry.ts | 48 +++ packages/ai/src/model/ModelRepository.ts | 153 +++++++-- packages/ai/src/provider/ProviderRegistry.ts | 15 +- packages/ai/src/task/DownloadModelTask.ts | 25 +- packages/ai/src/task/base/JobQueueLlmTask.ts | 11 +- .../core/src/storage/base/KVRepository.ts | 21 +- .../indexeddb/IndexedDbTaskGraphRepository.ts | 6 +- .../IndexedDbTaskOutputRepository.ts | 16 +- .../indexeddb/base/IndexedDbKVRepository.ts | 2 +- .../inmemory/InMemoryModelRepository.ts | 38 ++- .../inmemory/base/InMemoryKVRepository.ts | 2 +- .../test/InMemoryModelRepository.test.ts | 0 .../src/bun/sqlite/SqliteModelRepository.ts | 39 ++- .../src/bun/sqlite/base/SqliteKVRepository.ts | 2 +- packages/storage/src/bun/sqlite/index.ts | 1 + .../postgres/base/PostgresKVRepository.ts | 2 +- .../src/util/base/BaseSqlKVRepository.ts | 2 +- packages/test/package.json | 2 +- packages/test/src/index.ts | 16 +- .../test/src/sample/MediaPipeModelSamples.ts | 61 +++- packages/test/src/sample/ONNXModelSamples.ts | 304 ++++++++++-------- 39 files changed, 792 insertions(+), 552 deletions(-) create mode 100644 packages/ai/src/model/ModelRegistry.ts create mode 100644 packages/storage/src/browser/inmemory/test/InMemoryModelRepository.test.ts diff --git a/docs/developers/01_getting_started.md b/docs/developers/01_getting_started.md index 7861f2d..e732354 100644 --- a/docs/developers/01_getting_started.md +++ b/docs/developers/01_getting_started.md @@ -57,7 +57,7 @@ registerHuggingfaceLocalTasksInMemory(); const builder = new TaskGraphBuilder(); builder - .DownloadModel({ model: "Xenova/LaMini-Flan-T5-783M" }) + .DownloadModel({ model: "ONNX Xenova/LaMini-Flan-T5-783M q8" }) .TextRewriter({ text: "The quick brown fox jumps over the lazy dog.", prompt: ["Rewrite the following text in reverse:", "Rewrite this to sound like a pirate:"], @@ -87,7 +87,9 @@ registerHuggingfaceLocalTasksInMemory(); // build and run graph const graph = new TaskGraph(); -graph.addTask(new DownloadModel({ id: "1", input: { model: "Xenova/LaMini-Flan-T5-783M" } })); +graph.addTask( + new DownloadModel({ id: "1", input: { model: "ONNX Xenova/LaMini-Flan-T5-783M q8" } }) +); graph.addTask( new TextRewriterCompoundTask({ id: "2", @@ -284,7 +286,7 @@ There is a JSONTask that can be used to build a graph. This is useful for saving "id": "1", "type": "DownloadModelCompoundTask", "input": { - "model": ["Xenova/LaMini-Flan-T5-783M", "Xenova/m2m100_418M"] + "model": ["ONNX Xenova/LaMini-Flan-T5-783M q8", "ONNX Xenova/m2m100_418M q8"] } }, { @@ -305,7 +307,7 @@ There is a JSONTask that can be used to build a graph. This is useful for saving "id": "3", "type": "TextTranslationCompoundTask", "input": { - "model": "Xenova/m2m100_418M", + "model": "ONNX Xenova/m2m100_418M q8", "source": "en", "target": "es" }, diff --git a/examples/cli/src/TaskCLI.ts b/examples/cli/src/TaskCLI.ts index 4501f46..cce0aac 100644 --- a/examples/cli/src/TaskCLI.ts +++ b/examples/cli/src/TaskCLI.ts @@ -9,16 +9,12 @@ import { Command } from "commander"; import { runTask } from "./TaskStreamToListr2"; import "@huggingface/transformers"; import { TaskGraph, JsonTask, TaskGraphBuilder, JsonTaskItem } from "ellmers-core"; - -import { - DownloadModelTask, - DownloadModelCompoundTask, - findModelByName, - findModelByUseCase, - ModelUseCaseEnum, -} from "ellmers-ai"; +import { DownloadModelTask, getGlobalModelRepository } from "ellmers-ai"; +import { registerHuggingfaceLocalModels } from "ellmers-test"; import "ellmers-task"; +registerHuggingfaceLocalModels(); + export function AddBaseCommands(program: Command) { program .command("download") @@ -27,7 +23,7 @@ export function AddBaseCommands(program: Command) { .action(async (options) => { const graph = new TaskGraph(); if (options.model) { - const model = findModelByName(options.model); + const model = getGlobalModelRepository().findByName(options.model); if (model) { graph.addTask(new DownloadModelTask({ input: { model: model.name } })); } else { @@ -44,8 +40,10 @@ export function AddBaseCommands(program: Command) { .option("--model ", "model to use") .action(async (text: string, options) => { const model = options.model - ? findModelByName(options.model)?.name - : findModelByUseCase(ModelUseCaseEnum.TEXT_EMBEDDING).map((m) => m.name); + ? getGlobalModelRepository().findByName(options.model)?.name + : getGlobalModelRepository() + .findModelsByTask("TextEmbeddingTask") + .map((m) => m.name); if (!model) { program.error(`Unknown model ${options.model}`); } else { @@ -62,8 +60,10 @@ export function AddBaseCommands(program: Command) { .option("--model ", "model to use") .action(async (text, options) => { const model = options.model - ? findModelByName(options.model)?.name - : findModelByUseCase(ModelUseCaseEnum.TEXT_SUMMARIZATION).map((m) => m.name); + ? getGlobalModelRepository().findByName(options.model)?.name + : getGlobalModelRepository() + .findModelsByTask("TextSummaryTask") + .map((m) => m.name); if (!model) { program.error(`Unknown model ${options.model}`); } else { @@ -81,8 +81,10 @@ export function AddBaseCommands(program: Command) { .option("--model ", "model to use") .action(async (text, options) => { const model = options.model - ? findModelByName(options.model)?.name - : findModelByUseCase(ModelUseCaseEnum.TEXT_REWRITING).map((m) => m.name); + ? getGlobalModelRepository().findByName(options.model)?.name + : getGlobalModelRepository() + .findModelsByTask("TextRewriterTask") + .map((m) => m.name); if (!model) { program.error(`Unknown model ${options.model}`); } else { @@ -103,7 +105,7 @@ export function AddBaseCommands(program: Command) { id: "1", type: "DownloadModelTask", input: { - model: "Xenova/LaMini-Flan-T5-783M", + model: "ONNX Xenova/LaMini-Flan-T5-783M q8", }, }, { diff --git a/examples/web/package.json b/examples/web/package.json index 0d1fa09..59844d3 100644 --- a/examples/web/package.json +++ b/examples/web/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "concurrently --kill-others -c 'auto' -n app,types 'bunx --bun vite' 'tsc -w --noEmit'", + "dev": "concurrently --kill-others -c 'auto' -n app,types 'bunx --bun vite' 'tsc -w --noEmit --preserveWatchOutput'", "build": "vite build && tsc --noEmit", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" @@ -24,7 +24,8 @@ "ellmers-core": "workspace:packages/core", "ellmers-storage": "workspace:packages/storage", "ellmers-ai-provider": "workspace:packages/ai-provider", - "ellmers-ai": "workspace:packages/ai" + "ellmers-ai": "workspace:packages/ai", + "ellmers-test": "workspace:packages/test" }, "devDependencies": { "@types/react": "^19.0.4", diff --git a/examples/web/src/App.tsx b/examples/web/src/App.tsx index decd208..a3928c5 100644 --- a/examples/web/src/App.tsx +++ b/examples/web/src/App.tsx @@ -12,7 +12,6 @@ import { TaskOutput, } from "ellmers-core"; import { - IndexedDbQueue, IndexedDbTaskGraphRepository, IndexedDbTaskOutputRepository, } from "ellmers-storage/browser/indexeddb"; @@ -21,26 +20,37 @@ import { QueuesStatus } from "./QueueSatus"; import { OutputRepositoryStatus } from "./OutputRepositoryStatus"; import { GraphStoreStatus } from "./GraphStoreStatus"; import { InMemoryJobQueue } from "ellmers-storage/inmemory"; -import { registerHuggingfaceLocalTasks } from "ellmers-ai-provider/hf-transformers/browser"; -import { getProviderRegistry, ModelProviderEnum } from "ellmers-ai"; -import { registerMediaPipeTfJsLocalTasks } from "ellmers-ai-provider/tf-mediapipe/browser"; +import { getProviderRegistry } from "ellmers-ai"; +import { + LOCAL_ONNX_TRANSFORMERJS, + registerHuggingfaceLocalTasks, +} from "ellmers-ai-provider/hf-transformers/browser"; +import { + MEDIA_PIPE_TFJS_MODEL, + registerMediaPipeTfJsLocalTasks, +} from "ellmers-ai-provider/tf-mediapipe/browser"; import "ellmers-task"; import "ellmers-test"; +import { registerMediaPipeTfJsLocalModels } from "ellmers-test"; +import { registerHuggingfaceLocalModels } from "ellmers-test"; const ProviderRegistry = getProviderRegistry(); registerHuggingfaceLocalTasks(); ProviderRegistry.registerQueue( - ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, + LOCAL_ONNX_TRANSFORMERJS, new InMemoryJobQueue("local_hft", new ConcurrencyLimiter(1, 10), 10) ); registerMediaPipeTfJsLocalTasks(); ProviderRegistry.registerQueue( - ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL, + MEDIA_PIPE_TFJS_MODEL, new InMemoryJobQueue("local_mp", new ConcurrencyLimiter(1, 10), 10) ); +registerHuggingfaceLocalModels(); +registerMediaPipeTfJsLocalModels(); + ProviderRegistry.clearQueues(); ProviderRegistry.startQueues(); @@ -59,13 +69,13 @@ const graph = await taskGraphRepo.getTaskGraph("default"); const resetGraph = () => { builder .reset() - .DownloadModel({ model: ["Xenova/LaMini-Flan-T5-783M", "Xenova/m2m100_418M"] }) + .DownloadModel({ model: ["ONNX Xenova/LaMini-Flan-T5-783M q8", "ONNX Xenova/m2m100_418M q8"] }) .TextRewriter({ text: "The quick brown fox jumps over the lazy dog.", prompt: ["Rewrite the following text in reverse:", "Rewrite this to sound like a pirate:"], }) .TextTranslation({ - model: "Xenova/m2m100_418M", + model: "ONNX Xenova/m2m100_418M q8", source: "en", target: "es", }) diff --git a/examples/web/src/QueueSatus.tsx b/examples/web/src/QueueSatus.tsx index 4bc7c7e..1af21b2 100644 --- a/examples/web/src/QueueSatus.tsx +++ b/examples/web/src/QueueSatus.tsx @@ -1,8 +1,8 @@ import { JobStatus } from "ellmers-core"; -import { ModelProviderEnum, getProviderRegistry } from "ellmers-ai"; +import { getProviderRegistry } from "ellmers-ai"; import { useCallback, useEffect, useState } from "react"; -export function QueueStatus({ queueType }: { queueType: ModelProviderEnum }) { +export function QueueStatus({ queueType }: { queueType: string }) { const queue = getProviderRegistry().getQueue(queueType); const [pending, setPending] = useState(0); const [processing, setProcessing] = useState(0); diff --git a/examples/web/src/main.tsx b/examples/web/src/main.tsx index 4f3c534..80d7c70 100644 --- a/examples/web/src/main.tsx +++ b/examples/web/src/main.tsx @@ -1,6 +1,6 @@ import ReactDOM from "react-dom/client"; import { App } from "./App"; -import { TaskGraphBuilder } from "ellmers-core"; +import { TaskGraphBuilder, TaskRegistry } from "ellmers-core"; import "./main.css"; import { TaskConsoleFormatter, @@ -8,6 +8,7 @@ import { TaskGraphBuilderHelperConsoleFormatter, isDarkMode, } from "./ConsoleFormatters"; +import { getGlobalModelRepository } from "ellmers-ai"; ReactDOM.createRoot(document.getElementById("root")!).render( // @@ -39,7 +40,7 @@ console.log( ` %cbuilder.%creset%c(); - builder.%cDownloadModel%c({ %cmodel%c: [%c'Xenova/LaMini-Flan-T5-783M']%c }); + builder.%cDownloadModel%c({ %cmodel%c: [%c'ONNX Xenova/LaMini-Flan-T5-783M q8']%c }); builder.%cTextRewriter%c({ %ctext%c: %c'The quick brown fox jumps over the lazy dog.'%c, %cprompt%c: [%c'Rewrite the following text in reverse:'%c, %c'Rewrite this to sound like a pirate:'%c] }); builder.%crename%c(%c'text'%c, %c'message'%c); builder.%cDebugLog%c({ %clevel%c: %c'info'%c }); @@ -85,3 +86,13 @@ console.log( `color: ${grey}; font-weight: normal;` ); console.log(window["builder"]); + +console.log( + "Models Available: ", + getGlobalModelRepository().models.map((m) => m.name) +); + +console.log( + "Tasks Available: ", + Array.from(TaskRegistry.all.entries()).map(([name]) => name) +); diff --git a/examples/web/tsconfig.json b/examples/web/tsconfig.json index 3201c0a..f8d7029 100644 --- a/examples/web/tsconfig.json +++ b/examples/web/tsconfig.json @@ -22,7 +22,8 @@ "ellmers-core": ["../../packages/core/src"], "ellmers-ai-provider": ["../../packages/ai-provider/src"], "ellmers-storage": ["../../packages/storage/src"], - "ellmers-task": ["../../packages/task/src"] + "ellmers-task": ["../../packages/task/src"], + "ellmers-test": ["../../packages/test/src"] } }, "include": ["src"], @@ -31,6 +32,7 @@ { "path": "../../packages/core" }, { "path": "../../packages/task" }, { "path": "../../packages/ai-provider" }, - { "path": "../../packages/storage" } + { "path": "../../packages/storage" }, + { "path": "../../packages/test" } ] } diff --git a/packages/ai-provider/src/ggml/model/GgmlLocalModel.ts b/packages/ai-provider/src/ggml/model/GgmlLocalModel.ts index 9093ede..bf5fd30 100644 --- a/packages/ai-provider/src/ggml/model/GgmlLocalModel.ts +++ b/packages/ai-provider/src/ggml/model/GgmlLocalModel.ts @@ -5,11 +5,4 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { Model, ModelProviderEnum, ModelUseCaseEnum } from "../../../../ai/src/model/Model"; - -export class GgmlLocalModel extends Model { - constructor(name: string, useCase: ModelUseCaseEnum[], options?: ModelOptions) { - super(name, useCase, options); - } - readonly type = ModelProviderEnum.LOCAL_LLAMACPP; -} +export const LOCAL_LLAMACPP = "LOCAL_LLAMACPP"; diff --git a/packages/ai-provider/src/hf-transformers/bindings/registerTasks.ts b/packages/ai-provider/src/hf-transformers/bindings/registerTasks.ts index 64054a5..77571c5 100644 --- a/packages/ai-provider/src/hf-transformers/bindings/registerTasks.ts +++ b/packages/ai-provider/src/hf-transformers/bindings/registerTasks.ts @@ -1,5 +1,4 @@ import { - ModelProviderEnum, getProviderRegistry, DownloadModelTask, TextEmbeddingTask, @@ -18,49 +17,50 @@ import { HuggingFaceLocal_TextSummaryRun, HuggingFaceLocal_TextTranslationRun, } from "../provider/HuggingFaceLocal_TaskRun"; +import { LOCAL_ONNX_TRANSFORMERJS } from "../model/ONNXTransformerJsModel"; export async function registerHuggingfaceLocalTasks() { const ProviderRegistry = getProviderRegistry(); ProviderRegistry.registerRunFn( DownloadModelTask.type, - ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, + LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_DownloadRun ); ProviderRegistry.registerRunFn( TextEmbeddingTask.type, - ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, + LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_EmbeddingRun ); ProviderRegistry.registerRunFn( TextGenerationTask.type, - ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, + LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_TextGenerationRun ); ProviderRegistry.registerRunFn( TextTranslationTask.type, - ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, + LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_TextTranslationRun ); ProviderRegistry.registerRunFn( TextRewriterTask.type, - ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, + LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_TextRewriterRun ); ProviderRegistry.registerRunFn( TextSummaryTask.type, - ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, + LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_TextSummaryRun ); ProviderRegistry.registerRunFn( TextQuestionAnswerTask.type, - ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, + LOCAL_ONNX_TRANSFORMERJS, HuggingFaceLocal_TextQuestionAnswerRun ); } diff --git a/packages/ai-provider/src/hf-transformers/model/ONNXTransformerJsModel.ts b/packages/ai-provider/src/hf-transformers/model/ONNXTransformerJsModel.ts index 6f4fa4b..1d21430 100644 --- a/packages/ai-provider/src/hf-transformers/model/ONNXTransformerJsModel.ts +++ b/packages/ai-provider/src/hf-transformers/model/ONNXTransformerJsModel.ts @@ -5,7 +5,7 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { Model, ModelDetail, ModelProviderEnum, ModelUseCaseEnum } from "ellmers-ai"; +export const LOCAL_ONNX_TRANSFORMERJS = "LOCAL_ONNX_TRANSFORMERJS"; export enum QUANTIZATION_DATA_TYPES { auto = "auto", // Auto-detect based on environment @@ -18,24 +18,3 @@ export enum QUANTIZATION_DATA_TYPES { bnb4 = "bnb4", q4f16 = "q4f16", // fp16 model with int4 block weight quantization } - -export interface ONNXTransformerJsModelOptions extends ModelDetail { - quantization?: QUANTIZATION_DATA_TYPES; -} - -export class ONNXTransformerJsModel extends Model implements ONNXTransformerJsModelOptions { - constructor( - name: string, - useCase: ModelUseCaseEnum[], - public pipeline: string, - options?: Pick< - ONNXTransformerJsModelOptions, - "dimensions" | "parameters" | "languageStyle" | "dtype" - > - ) { - super(name, useCase, options); - this.dtype = options?.dtype ?? DATA_TYPES.q8; - } - readonly type = ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS; - dtype?: DATA_TYPES | { [key: string]: DATA_TYPES } | undefined; -} diff --git a/packages/ai-provider/src/hf-transformers/provider/HuggingFaceLocal_TaskRun.ts b/packages/ai-provider/src/hf-transformers/provider/HuggingFaceLocal_TaskRun.ts index c0bde04..f4913e9 100644 --- a/packages/ai-provider/src/hf-transformers/provider/HuggingFaceLocal_TaskRun.ts +++ b/packages/ai-provider/src/hf-transformers/provider/HuggingFaceLocal_TaskRun.ts @@ -21,8 +21,8 @@ import { TextStreamer, } from "@huggingface/transformers"; import { ElVector } from "ellmers-core"; -import { ONNXTransformerJsModel } from "../model/ONNXTransformerJsModel"; -import { findModelByName } from "ellmers-ai"; + +import { getGlobalModelRepository } from "ellmers-ai"; import type { JobQueueLlmTask, DownloadModelTask, @@ -46,7 +46,9 @@ import type { TextTranslationTask, TextTranslationTaskInput, TextTranslationTaskOutput, + Model, } from "ellmers-ai"; +import { QUANTIZATION_DATA_TYPES } from "../browser"; env.cacheDir = "./.cache"; @@ -83,7 +85,7 @@ type StatusFile = StatusFileBookends | StatusFileProgress; type StatusRun = StatusRunReady | StatusRunUpdate | StatusRunComplete; export type CallbackStatus = StatusFile | StatusRun; -const pipelines = new Map(); +const pipelines = new Map(); /** * @@ -94,16 +96,12 @@ const pipelines = new Map(); * @param model * @param options */ -const getPipeline = async ( - task: JobQueueLlmTask, - model: ONNXTransformerJsModel, - options: any = {} -) => { +const getPipeline = async (task: JobQueueLlmTask, model: Model, options: any = {}) => { if (!pipelines.has(model)) { pipelines.set( model, - await pipeline(model.pipeline as PipelineType, model.name, { - dtype: model.dtype || "q8", + await pipeline(model.pipeline as PipelineType, model.url, { + dtype: (model.quantization as QUANTIZATION_DATA_TYPES) || "q8", session_options: options?.session_options, progress_callback: downloadProgressCallback(task), }) @@ -142,9 +140,9 @@ export async function HuggingFaceLocal_DownloadRun( task: DownloadModelTask, runInputData: DownloadModelTaskInput ): Promise> { - const model = findModelByName(runInputData.model)! as ONNXTransformerJsModel; + const model = getGlobalModelRepository().findByName(runInputData.model)!; await getPipeline(task, model); - return { model: model.name, dimensions: model.dimensions || 0, normalize: model.normalize }; + return { model: model.name, dimensions: model.nativeDimensions || 0, normalize: model.normalize }; } /** @@ -156,7 +154,7 @@ export async function HuggingFaceLocal_EmbeddingRun( task: TextEmbeddingTask, runInputData: TextEmbeddingTaskInput ): Promise { - const model = findModelByName(runInputData.model) as ONNXTransformerJsModel; + const model = getGlobalModelRepository().findByName(runInputData.model)!; const generateEmbedding: FeatureExtractionPipeline = await getPipeline(task, model); const hfVector = await generateEmbedding(runInputData.text, { @@ -164,15 +162,15 @@ export async function HuggingFaceLocal_EmbeddingRun( normalize: model.normalize, }); - if (hfVector.size !== model.dimensions) { + if (hfVector.size !== model.nativeDimensions) { console.warn( - `HuggingFaceLocal Embedding vector length does not match model dimensions v${hfVector.size} != m${model.dimensions}`, + `HuggingFaceLocal Embedding vector length does not match model dimensions v${hfVector.size} != m${model.nativeDimensions}`, runInputData, hfVector ); - throw `HuggingFaceLocal Embedding vector length does not match model dimensions v${hfVector.size} != m${model.dimensions}`; + throw `HuggingFaceLocal Embedding vector length does not match model dimensions v${hfVector.size} != m${model.nativeDimensions}`; } - const vector = new ElVector(hfVector.data, model.normalize); + const vector = new ElVector(hfVector.data, model.normalize ?? true); return { vector }; } @@ -185,7 +183,7 @@ export async function HuggingFaceLocal_TextGenerationRun( task: TextGenerationTask, runInputData: TextGenerationTaskInput ): Promise { - const model = findModelByName(runInputData.model) as ONNXTransformerJsModel; + const model = getGlobalModelRepository().findByName(runInputData.model)!; const generateText: TextGenerationPipeline = await getPipeline(task, model); @@ -221,7 +219,7 @@ export async function HuggingFaceLocal_TextTranslationRun( task: TextTranslationTask, runInputData: TextTranslationTaskInput ): Promise> { - const model = findModelByName(runInputData.model) as ONNXTransformerJsModel; + const model = getGlobalModelRepository().findByName(runInputData.model)!; const translate: TranslationPipeline = await getPipeline(task, model); @@ -253,7 +251,7 @@ export async function HuggingFaceLocal_TextRewriterRun( task: TextRewriterTask, runInputData: TextRewriterTaskInput ): Promise { - const model = findModelByName(runInputData.model) as ONNXTransformerJsModel; + const model = getGlobalModelRepository().findByName(runInputData.model)!; const generateText: TextGenerationPipeline = await getPipeline(task, model); const streamer = new TextStreamer(generateText.tokenizer, { @@ -292,7 +290,7 @@ export async function HuggingFaceLocal_TextSummaryRun( task: TextSummaryTask, runInputData: TextSummaryTaskInput ): Promise { - const model = findModelByName(runInputData.model) as ONNXTransformerJsModel; + const model = getGlobalModelRepository().findByName(runInputData.model)!; const generateSummary: SummarizationPipeline = await getPipeline(task, model); const streamer = new TextStreamer(generateSummary.tokenizer, { @@ -321,7 +319,7 @@ export async function HuggingFaceLocal_TextQuestionAnswerRun( task: TextQuestionAnswerTask, runInputData: TextQuestionAnswerTaskInput ): Promise { - const model = findModelByName(runInputData.model) as ONNXTransformerJsModel; + const model = getGlobalModelRepository().findByName(runInputData.model)!; const generateAnswer: QuestionAnsweringPipeline = await getPipeline(task, model); const streamer = new TextStreamer(generateAnswer.tokenizer, { diff --git a/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts b/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts index 7d31de8..0251083 100644 --- a/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts +++ b/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts @@ -7,25 +7,35 @@ import { describe, expect, it } from "bun:test"; import { ConcurrencyLimiter, TaskGraphBuilder, TaskInput, TaskOutput } from "ellmers-core"; -import { getProviderRegistry, ModelProviderEnum, ModelUseCaseEnum } from "ellmers-ai"; +import { getProviderRegistry, getGlobalModelRepository } from "ellmers-ai"; import { InMemoryJobQueue } from "ellmers-storage/inmemory"; -import { SqliteJobQueue } from "ellmers-storage/bun/sqlite"; +import { getDatabase, SqliteJobQueue } from "ellmers-storage/bun/sqlite"; import { registerHuggingfaceLocalTasks } from "../bindings/registerTasks"; -import { getDatabase } from "../../../../storage/src/util/db_sqlite"; import { sleep } from "bun"; -import { ONNXTransformerJsModel } from "../model/ONNXTransformerJsModel"; +import { LOCAL_ONNX_TRANSFORMERJS } from "../model/ONNXTransformerJsModel"; const HFQUEUE = "local_hf"; describe("HFTransformersBinding", () => { describe("InMemoryJobQueue", () => { it("Should have an item queued", async () => { - // the model gets self-registered - const flanT5p786m = new ONNXTransformerJsModel( - "Xenova/LaMini-Flan-T5-783M", - [ModelUseCaseEnum.TEXT_GENERATION, ModelUseCaseEnum.TEXT_REWRITING], - "text2text-generation" + getGlobalModelRepository().addModel({ + name: "ONNX Xenova/LaMini-Flan-T5-783M q8", + url: "Xenova/LaMini-Flan-T5-783M", + availableOnBrowser: true, + availableOnServer: true, + provider: LOCAL_ONNX_TRANSFORMERJS, + pipeline: "text2text-generation", + }); + getGlobalModelRepository().connectTaskToModel( + "TextGenerationTask", + "ONNX Xenova/LaMini-Flan-T5-783M q8" + ); + getGlobalModelRepository().connectTaskToModel( + "TextRewritingTask", + "ONNX Xenova/LaMini-Flan-T5-783M q8" ); + registerHuggingfaceLocalTasks(); const providerRegistry = getProviderRegistry(); const jobQueue = new InMemoryJobQueue( @@ -33,14 +43,14 @@ describe("HFTransformersBinding", () => { new ConcurrencyLimiter(1, 10), 10 ); - providerRegistry.registerQueue(ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); - const queue = providerRegistry.getQueue(ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS); + providerRegistry.registerQueue(LOCAL_ONNX_TRANSFORMERJS, jobQueue); + const queue = providerRegistry.getQueue(LOCAL_ONNX_TRANSFORMERJS); expect(queue).toBeDefined(); expect(queue?.queue).toEqual(HFQUEUE); const builder = new TaskGraphBuilder(); builder.DownloadModel({ - model: "Xenova/LaMini-Flan-T5-783M", + model: "ONNX Xenova/LaMini-Flan-T5-783M q8", }); builder.run(); await sleep(1); @@ -54,20 +64,20 @@ describe("HFTransformersBinding", () => { registerHuggingfaceLocalTasks(); const providerRegistry = getProviderRegistry(); const jobQueue = new SqliteJobQueue( - getDatabase(), + getDatabase(":memory:"), HFQUEUE, new ConcurrencyLimiter(1, 10), 10 ); jobQueue.ensureTableExists(); - providerRegistry.registerQueue(ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); - const queue = providerRegistry.getQueue(ModelProviderEnum.LOCAL_ONNX_TRANSFORMERJS); + providerRegistry.registerQueue(LOCAL_ONNX_TRANSFORMERJS, jobQueue); + const queue = providerRegistry.getQueue(LOCAL_ONNX_TRANSFORMERJS); expect(queue).toBeDefined(); expect(queue?.queue).toEqual(HFQUEUE); const builder = new TaskGraphBuilder(); builder.DownloadModel({ - model: "Xenova/LaMini-Flan-T5-783M", + model: "ONNX Xenova/LaMini-Flan-T5-783M q8", }); builder.run(); await sleep(1); diff --git a/packages/ai-provider/src/tf-mediapipe/bindings/registerTasks.ts b/packages/ai-provider/src/tf-mediapipe/bindings/registerTasks.ts index 4b4277f..06cdf81 100644 --- a/packages/ai-provider/src/tf-mediapipe/bindings/registerTasks.ts +++ b/packages/ai-provider/src/tf-mediapipe/bindings/registerTasks.ts @@ -1,22 +1,23 @@ -import { ModelProviderEnum, getProviderRegistry } from "ellmers-ai"; +import { getProviderRegistry } from "ellmers-ai"; import { DownloadModelTask, TextEmbeddingTask } from "ellmers-ai"; import { MediaPipeTfJsLocal_Download, MediaPipeTfJsLocal_Embedding, } from "../provider/MediaPipeLocalTaskRun"; +import { MEDIA_PIPE_TFJS_MODEL } from "../browser"; export const registerMediaPipeTfJsLocalTasks = () => { const ProviderRegistry = getProviderRegistry(); ProviderRegistry.registerRunFn( DownloadModelTask.type, - ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL, + MEDIA_PIPE_TFJS_MODEL, MediaPipeTfJsLocal_Download ); ProviderRegistry.registerRunFn( TextEmbeddingTask.type, - ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL, + MEDIA_PIPE_TFJS_MODEL, MediaPipeTfJsLocal_Embedding ); }; diff --git a/packages/ai-provider/src/tf-mediapipe/model/MediaPipeModel.ts b/packages/ai-provider/src/tf-mediapipe/model/MediaPipeModel.ts index 60e2584..5280a7e 100644 --- a/packages/ai-provider/src/tf-mediapipe/model/MediaPipeModel.ts +++ b/packages/ai-provider/src/tf-mediapipe/model/MediaPipeModel.ts @@ -5,16 +5,4 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { Model, ModelOptions, ModelProviderEnum, ModelUseCaseEnum } from "ellmers-ai"; - -export class MediaPipeTfJsModel extends Model { - constructor( - name: string, - useCase: ModelUseCaseEnum[], - public url: string, - options?: Pick - ) { - super(name, useCase, options); - } - readonly type = ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL; -} +export const MEDIA_PIPE_TFJS_MODEL = "MEDIA_PIPE_TFJS_MODEL"; diff --git a/packages/ai-provider/src/tf-mediapipe/provider/MediaPipeLocalTaskRun.ts b/packages/ai-provider/src/tf-mediapipe/provider/MediaPipeLocalTaskRun.ts index c23bbf9..2a767fa 100644 --- a/packages/ai-provider/src/tf-mediapipe/provider/MediaPipeLocalTaskRun.ts +++ b/packages/ai-provider/src/tf-mediapipe/provider/MediaPipeLocalTaskRun.ts @@ -8,13 +8,12 @@ import { FilesetResolver, TextEmbedder } from "@mediapipe/tasks-text"; import { ElVector } from "ellmers-core"; import { - findModelByName, DownloadModelTask, DownloadModelTaskInput, TextEmbeddingTask, TextEmbeddingTaskInput, + getGlobalModelRepository, } from "ellmers-ai"; -import { MediaPipeTfJsModel } from "../model/MediaPipeModel"; /** * This is a task that downloads and caches a MediaPipe TFJS model. @@ -26,10 +25,13 @@ export async function MediaPipeTfJsLocal_Download( const textFiles = await FilesetResolver.forTextTasks( "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-text@latest/wasm" ); - const model = findModelByName(runInputData.model) as MediaPipeTfJsModel; + const model = getGlobalModelRepository().findByName(runInputData.model); + if (!model) { + throw `MediaPipeTfJsLocal_Download: Model ${runInputData.model} not found`; + } const results = await TextEmbedder.createFromOptions(textFiles, { baseOptions: { - modelAssetPath: model.url, + modelAssetPath: model.url!, }, quantize: true, }); @@ -48,10 +50,13 @@ export async function MediaPipeTfJsLocal_Embedding( const textFiles = await FilesetResolver.forTextTasks( "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-text@latest/wasm" ); - const model = findModelByName(runInputData.model) as MediaPipeTfJsModel; + const model = getGlobalModelRepository().findByName(runInputData.model); + if (!model) { + throw `MediaPipeTfJsLocal_Embedding: Model ${runInputData.model} not found`; + } const textEmbedder = await TextEmbedder.createFromOptions(textFiles, { baseOptions: { - modelAssetPath: model.url, + modelAssetPath: model.url!, }, quantize: true, }); @@ -59,8 +64,8 @@ export async function MediaPipeTfJsLocal_Embedding( const output = textEmbedder.embed(runInputData.text); const vector = output.embeddings[0].floatEmbedding; - if (vector?.length !== model.dimensions) { - throw `MediaPipeTfJsLocal Embedding vector length does not match model dimensions v${vector?.length} != m${model.dimensions}`; + if (vector?.length !== model.nativeDimensions) { + throw `MediaPipeTfJsLocal Embedding vector length does not match model dimensions v${vector?.length} != m${model.nativeDimensions}`; } return { vector: vector ? new ElVector(vector, true) : null }; } diff --git a/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts b/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts index 890f842..3d08631 100644 --- a/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts +++ b/packages/ai-provider/src/tf-mediapipe/test/TfMediaPipeBinding.test.ts @@ -1,88 +1,102 @@ -// ******************************************************************************* -// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * -// * * -// * Copyright Steven Roussey * -// * Licensed under the Apache License, Version 2.0 (the "License"); * -// ******************************************************************************* +// // ******************************************************************************* +// // * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// // * * +// // * Copyright Steven Roussey * +// // * Licensed under the Apache License, Version 2.0 (the "License"); * +// // ******************************************************************************* -import { describe, expect, it } from "bun:test"; -import { ConcurrencyLimiter, TaskGraphBuilder, TaskInput, TaskOutput } from "ellmers-core"; -import { getProviderRegistry, ModelProviderEnum, ModelUseCaseEnum } from "ellmers-ai"; -import { InMemoryJobQueue } from "ellmers-storage/inmemory"; -import { SqliteJobQueue } from "ellmers-storage/bun/sqlite"; -import { registerMediaPipeTfJsLocalTasks } from "../bindings/registerTasks"; -import { sleep } from "ellmers-core"; -import { MediaPipeTfJsModel } from "../model/MediaPipeModel"; -import { getDatabase } from "../../../../storage/src/util/db_sqlite"; +// import { describe, expect, it } from "bun:test"; +// import { ConcurrencyLimiter, TaskGraphBuilder, TaskInput, TaskOutput } from "ellmers-core"; +// import { getGlobalModelRepository, getProviderRegistry, Model } from "ellmers-ai"; +// import { InMemoryJobQueue } from "ellmers-storage/inmemory"; +// import { SqliteJobQueue } from "../../../../storage/dist/bun/sqlite"; +// import { registerMediaPipeTfJsLocalTasks } from "../bindings/registerTasks"; +// import { sleep } from "ellmers-core"; +// import { MEDIA_PIPE_TFJS_MODEL } from "../model/MediaPipeModel"; +// import { getDatabase } from "../../../../storage/src/util/db_sqlite"; -const TFQUEUE = "local_tf-mediapipe"; +// const TFQUEUE = "local_tf-mediapipe"; -describe("TfMediaPipeBinding", () => { - describe("InMemoryJobQueue", () => { - it("should not fail", async () => { - // register on creation - const universal_sentence_encoder = new MediaPipeTfJsModel( - "Universal Sentence Encoder", - [ModelUseCaseEnum.TEXT_EMBEDDING], - "https://storage.googleapis.com/mediapipe-tasks/text_embedder/universal_sentence_encoder.tflite", - { dimensions: 100, browserOnly: true } - ); - registerMediaPipeTfJsLocalTasks(); - const ProviderRegistry = getProviderRegistry(); - const jobQueue = new InMemoryJobQueue( - TFQUEUE, - new ConcurrencyLimiter(1, 10), - 10 - ); - ProviderRegistry.registerQueue(ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); - const queue = ProviderRegistry.getQueue(ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL); - expect(queue).toBeDefined(); - expect(queue?.queue).toEqual(TFQUEUE); +// describe("TfMediaPipeBinding", () => { +// describe("InMemoryJobQueue", () => { +// it("should not fail", async () => { +// // register on creation +// const universal_sentence_encoder: Model = { +// name: "Universal Sentence Encoder", +// url: "https://storage.googleapis.com/mediapipe-tasks/text_embedder/universal_sentence_encoder.tflite", +// nativeDimensions: 100, +// availableOnBrowser: true, +// availableOnServer: false, +// provider: MEDIA_PIPE_TFJS_MODEL, +// }; +// getGlobalModelRepository().addModel(universal_sentence_encoder); +// getGlobalModelRepository().connectTaskToModel( +// "TextEmbeddingTask", +// universal_sentence_encoder.name +// ); +// registerMediaPipeTfJsLocalTasks(); +// const ProviderRegistry = getProviderRegistry(); +// const jobQueue = new InMemoryJobQueue( +// TFQUEUE, +// new ConcurrencyLimiter(1, 10), +// 10 +// ); +// ProviderRegistry.registerQueue(MEDIA_PIPE_TFJS_MODEL, jobQueue); +// const queue = ProviderRegistry.getQueue(MEDIA_PIPE_TFJS_MODEL); +// expect(queue).toBeDefined(); +// expect(queue?.queue).toEqual(TFQUEUE); - const builder = new TaskGraphBuilder(); - builder.DownloadModel({ - model: "Universal Sentence Encoder", - }); - builder.run(); - await sleep(1); - // we are not in a browser context, so the model should not be registered - expect(await queue?.size()).toEqual(0); - builder.reset(); - await queue?.clear(); - }); - }); - describe("SqliteJobQueue", () => { - it("should not fail", async () => { - const universal_sentence_encoder = new MediaPipeTfJsModel( - "Universal Sentence Encoder", - [ModelUseCaseEnum.TEXT_EMBEDDING], - "https://storage.googleapis.com/mediapipe-tasks/text_embedder/universal_sentence_encoder.tflite", - { dimensions: 100, browserOnly: true } - ); - registerMediaPipeTfJsLocalTasks(); - const ProviderRegistry = getProviderRegistry(); - const jobQueue = new SqliteJobQueue( - getDatabase(":memory:"), - TFQUEUE, - new ConcurrencyLimiter(1, 10), - 10 - ); - jobQueue.ensureTableExists(); - ProviderRegistry.registerQueue(ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); - const queue = ProviderRegistry.getQueue(ModelProviderEnum.MEDIA_PIPE_TFJS_MODEL); - expect(queue).toBeDefined(); - expect(queue?.queue).toEqual(TFQUEUE); +// const builder = new TaskGraphBuilder(); +// builder.DownloadModel({ +// model: "Universal Sentence Encoder", +// }); +// builder.run(); +// await sleep(1); +// // we are not in a browser context, so the model should not be registered +// expect(await queue?.size()).toEqual(0); +// builder.reset(); +// await queue?.clear(); +// }); +// }); +// describe("SqliteJobQueue", () => { +// it("should not fail", async () => { +// const universal_sentence_encoder: Model = { +// name: "Universal Sentence Encoder", +// url: "https://storage.googleapis.com/mediapipe-tasks/text_embedder/universal_sentence_encoder.tflite", +// nativeDimensions: 100, +// availableOnBrowser: true, +// availableOnServer: false, +// provider: MEDIA_PIPE_TFJS_MODEL, +// }; +// getGlobalModelRepository().addModel(universal_sentence_encoder); +// getGlobalModelRepository().connectTaskToModel( +// "TextEmbeddingTask", +// universal_sentence_encoder.name +// ); +// registerMediaPipeTfJsLocalTasks(); +// const ProviderRegistry = getProviderRegistry(); +// const jobQueue = new SqliteJobQueue( +// getDatabase(":memory:"), +// TFQUEUE, +// new ConcurrencyLimiter(1, 10), +// 10 +// ); +// jobQueue.ensureTableExists(); +// ProviderRegistry.registerQueue(MEDIA_PIPE_TFJS_MODEL, jobQueue); +// const queue = ProviderRegistry.getQueue(MEDIA_PIPE_TFJS_MODEL); +// expect(queue).toBeDefined(); +// expect(queue?.queue).toEqual(TFQUEUE); - const builder = new TaskGraphBuilder(); - builder.DownloadModel({ - model: "Universal Sentence Encoder", - }); - builder.run(); - await sleep(1); - // we are not in a browser context, so the model should not be registered - expect(await queue?.size()).toEqual(0); - builder.reset(); - await queue?.clear(); - }); - }); -}); +// const builder = new TaskGraphBuilder(); +// builder.DownloadModel({ +// model: "Universal Sentence Encoder", +// }); +// builder.run(); +// await sleep(1); +// // we are not in a browser context, so the model should not be registered +// expect(await queue?.size()).toEqual(0); +// builder.reset(); +// await queue?.clear(); +// }); +// }); +// }); diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index b01ef9a..f9a3441 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,4 +1,5 @@ export * from "./task"; export * from "./model/Model"; +export * from "./model/ModelRegistry"; export * from "./model/ModelRepository"; export * from "./provider/ProviderRegistry"; diff --git a/packages/ai/src/model/Model.ts b/packages/ai/src/model/Model.ts index fba8510..348d220 100644 --- a/packages/ai/src/model/Model.ts +++ b/packages/ai/src/model/Model.ts @@ -5,118 +5,27 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -export enum ModelProviderEnum { - LOCAL_ONNX_TRANSFORMERJS = "LOCAL_ONNX_TRANSFORMERJS", - MEDIA_PIPE_TFJS_MODEL = "MEDIA_PIPE_TFJS_MODEL", - LOCAL_MLC = "LOCAL_MLC", - LOCAL_LLAMACPP = "LOCAL_LLAMACPP", - ONLINE_HUGGINGFACE = "ONLINE_HUGGINGFACE", - ONLINE_OPENAI = "ONLINE_OPENAI", - ONLINE_REPLICATE = "ONLINE_REPLICATE", -} - -export enum ModelUseCaseEnum { - TEXT_EMBEDDING = "TEXT_EMBEDDING", - TEXT_REWRITING = "TEXT_REWRITING", - TEXT_GENERATION = "TEXT_GENERATION", - TEXT_SUMMARIZATION = "TEXT_SUMMARIZATION", - TEXT_QUESTION_ANSWERING = "TEXT_QUESTION_ANSWERING", - TEXT_CLASSIFICATION = "TEXT_CLASSIFICATION", - TEXT_TRANSLATION = "TEXT_TRANSLATION", -} - -export interface ModelPrimaryKey { +export type ModelPrimaryKey = { name: string; - provider: ModelProviderEnum; - quantization: string; -} +}; export const ModelPrimaryKeySchema = { name: "string", - provider: "string", - quantization: "string", } as const; -export interface ModelDetail { - useCase: ModelUseCaseEnum; - pipeline: string; - nativeDimensions: number; - usingDimensions: number; - contextWindow: number; +export type ModelDetail = { + url: string; + provider: string; availableOnBrowser: boolean; availableOnServer: boolean; - parameters: number; - languageStyle: string; -} - -export const ModelDetailSchema = { - useCase: "string", - pipeline: "string", - nativeDimensions: "number", - usingDimensions: "number", - contextWindow: "number", - availableOnBrowser: "boolean", - availableOnServer: "boolean", - parameters: "number", - languageStyle: "string", -} as const; - -export class Model implements ModelPrimaryKey, ModelDetail { - constructor( - public name: string, - public provider: ModelProviderEnum, - public quantization: string, - details: ModelDetail - ) { - this.useCase = details.useCase; - this.pipeline = details.pipeline; - this.nativeDimensions = details.nativeDimensions; - this.usingDimensions = details.usingDimensions; - this.contextWindow = details.contextWindow; - this.availableOnBrowser = details.availableOnBrowser; - this.availableOnServer = details.availableOnServer; - this.parameters = details.parameters; - this.languageStyle = details.languageStyle; - } - public useCase: ModelUseCaseEnum; - public pipeline: string; - public nativeDimensions: number; - public usingDimensions: number; - public contextWindow: number; - public availableOnBrowser: boolean; - public availableOnServer: boolean; - public parameters: number; - public languageStyle: string; -} - -// const runningOnServer = typeof (globalThis as any).window === "undefined"; - -// export interface ModelOptions { -// nativeDimensions?: number; // Matryoshka Representation Learning (MRL) -- can truncate embedding dimensions from native number -// dimensions?: number; -// contextWindow?: number; -// extras?: Record; -// browserOnly?: boolean; -// parameters?: number; -// languageStyle?: string; -// } - -// export abstract class Model implements ModelOptions { -// public dimensions?: number; -// public nativeDimensions?: number; -// public contextWindow?: number; -// public normalize: boolean = true; -// public browserOnly: boolean = false; -// public extras: Record = {}; -// public parameters?: number; -// constructor( -// public name: string, -// public useCase: ModelUseCaseEnum[] = [], -// options?: ModelOptions -// ) { -// Object.assign(this, options); -// } -// abstract readonly type: ModelProviderEnum; -// } - -// export type ModelList = Model[]; + quantization?: string; + pipeline?: string; + normalize?: boolean; + nativeDimensions?: number; + usingDimensions?: number; + contextWindow?: number; + numParameters?: number; + languageStyle?: string; +}; + +export type Model = ModelPrimaryKey & ModelDetail; diff --git a/packages/ai/src/model/ModelRegistry.ts b/packages/ai/src/model/ModelRegistry.ts new file mode 100644 index 0000000..354aaed --- /dev/null +++ b/packages/ai/src/model/ModelRegistry.ts @@ -0,0 +1,48 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { Model } from "./Model"; +import { Task2ModelPrimaryKey } from "./ModelRepository"; + +// temporary model registry that is synchronous until we have a proper model repository + +class ModelRegistry { + models: Model[] = []; + task2models: Task2ModelPrimaryKey[] = []; + + addModel(model: Model) { + if (this.models.some((m) => m.name === model.name)) { + this.models = this.models.filter((m) => m.name !== model.name); + } + + this.models.push(model); + } + findModelsByTask(task: string) { + return this.task2models + .filter((t2m) => t2m.task === task) + .map((t2m) => this.models.find((m) => m.name === t2m.model)) + .filter((m) => m !== undefined); + } + findTasksByModel(name: string) { + return this.task2models.filter((t2m) => t2m.model === name).map((t2m) => t2m.task); + } + findByName(name: string) { + return this.models.find((m) => m.name === name); + } + connectTaskToModel(task: string, model: string) { + this.task2models.push({ task, model }); + } +} + +let modelRegistry: ModelRegistry; +export function getGlobalModelRepository() { + if (!modelRegistry) modelRegistry = new ModelRegistry(); + return modelRegistry; +} +export function setGlobalModelRepository(pr: ModelRegistry) { + modelRegistry = pr; +} diff --git a/packages/ai/src/model/ModelRepository.ts b/packages/ai/src/model/ModelRepository.ts index ac10135..d61b913 100644 --- a/packages/ai/src/model/ModelRepository.ts +++ b/packages/ai/src/model/ModelRepository.ts @@ -6,46 +6,159 @@ // ******************************************************************************* import EventEmitter from "eventemitter3"; -import { KVRepository } from "ellmers-core"; -import { - Model, - ModelPrimaryKey, - ModelProviderEnum, - ModelUseCaseEnum, - ModelPrimaryKeySchema, -} from "./Model"; +import { DefaultValueType, KVRepository } from "ellmers-core"; +import { Model, ModelPrimaryKey, ModelPrimaryKeySchema } from "./Model"; -export type ModelEvents = "models_cleared"; +/** + * Events that can be emitted by the ModelRepository + * @typedef {string} ModelEvents + */ +export type ModelEvents = + | "model_added" + | "model_removed" + | "task_model_connected" + | "task_model_disconnected" + | "model_updated"; +/** + * Represents the primary key structure for mapping tasks to models + * @interface Task2ModelPrimaryKey + */ +export type Task2ModelPrimaryKey = { + /** The task identifier */ + task: string; + /** The model identifier */ + model: string; +}; + +export const Task2ModelPrimaryKeySchema = { + task: "string", + model: "string", +} as const; + +/** + * Schema definition for Task2ModelDetail + */ +export type Task2ModelDetail = { + /** Optional details about the task-model relationship */ + details: string | null; +}; + +export const Task2ModelDetailSchema = { + details: "string", +} as const; + +/** + * Abstract base class for managing AI models and their relationships with tasks. + * Provides functionality for storing, retrieving, and managing the lifecycle of models + * and their associations with specific tasks. + */ export abstract class ModelRepository { - public type = "TaskOutputRepository"; - abstract kvRepository: KVRepository; + /** Repository type identifier */ + public type = "ModelRepository"; + + /** + * Repository for storing and managing Model instances + */ + abstract modelKvRepository: KVRepository< + ModelPrimaryKey, + DefaultValueType, + typeof ModelPrimaryKeySchema + >; + + /** + * Repository for managing relationships between tasks and models + */ + abstract task2ModelKvRepository: KVRepository< + Task2ModelPrimaryKey, + Task2ModelDetail, + typeof Task2ModelPrimaryKeySchema, + typeof Task2ModelDetailSchema + >; + + /** Event emitter for repository events */ private events = new EventEmitter(); + + /** + * Registers an event listener for the specified event + * @param name - The event name to listen for + * @param fn - The callback function to execute when the event occurs + */ on(name: ModelEvents, fn: (...args: any[]) => void) { this.events.on.call(this.events, name, fn); } + + /** + * Removes an event listener for the specified event + * @param name - The event name to stop listening for + * @param fn - The callback function to remove + */ off(name: ModelEvents, fn: (...args: any[]) => void) { this.events.off.call(this.events, name, fn); } + + /** + * Emits an event with the specified name and arguments + * @param name - The event name to emit + * @param args - Arguments to pass to the event listeners + */ emit(name: ModelEvents, ...args: any[]) { this.events.emit.call(this.events, name, ...args); } - findByName(key: unknown) { - if (typeof key != "string") return undefined; - return this.kvRepository.getKeyValue(key); + /** + * Adds a new model to the repository + * @param model - The model instance to add + */ + async addModel(model: Model) { + await this.modelKvRepository.put({ name: model.name }, { "kv-value": JSON.stringify(model) }); + this.emit("model_added", model); + } + + /** + * Finds all models associated with a specific task + * @param task - The task identifier to search for + * @returns Promise resolving to an array of associated models, or undefined if none found + */ + async findModelByTask(task: string) { + if (typeof task != "string") return undefined; + const junctions = await this.task2ModelKvRepository.search({ task }); + if (!junctions || junctions.length === 0) return undefined; + const models = []; + for (const junction of junctions) { + const model = await this.modelKvRepository.getKeyValue({ name: junction.model }); + if (model) models.push(JSON.parse(model["kv-value"])); + } + return models; } - findByUseCase(usecase: ModelUseCaseEnum) { - return this.kvRepository.getKeyValue({ useCase: usecase.toLowerCase() }); + /** + * Creates an association between a task and a model + * @param task - The task identifier + * @param model - The model to associate with the task + */ + async connectModelToTask(task: string, model: Model) { + this.task2ModelKvRepository.put({ task, model: model.name }, { details: null }); + this.emit("task_model_connected", task, model); } - async clear(): Promise { - await this.kvRepository.deleteAll(); - this.emit("models_cleared"); + /** + * Retrieves a model by its name + * @param name - The name of the model to find + * @returns Promise resolving to the found model or undefined if not found + */ + async findByName(name: string) { + if (typeof name != "string") return undefined; + const modelstr = await this.modelKvRepository.getKeyValue({ name }); + if (!modelstr) return undefined; + return JSON.parse(modelstr["kv-value"]); } + /** + * Gets the total number of models in the repository + * @returns Promise resolving to the number of stored models + */ async size(): Promise { - return await this.kvRepository.size(); + return await this.modelKvRepository.size(); } } diff --git a/packages/ai/src/provider/ProviderRegistry.ts b/packages/ai/src/provider/ProviderRegistry.ts index f7f57e0..30a4799 100644 --- a/packages/ai/src/provider/ProviderRegistry.ts +++ b/packages/ai/src/provider/ProviderRegistry.ts @@ -5,7 +5,6 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import type { ModelProviderEnum } from "../model/Model"; import { Job, type JobQueue, @@ -41,14 +40,14 @@ export class ProviderRegistry { {}; registerRunFn( taskType: string, - modelType: ModelProviderEnum, + modelProvider: string, runFn: (task: any, runInputData: any) => Promise ) { if (!this.runFnRegistry[taskType]) this.runFnRegistry[taskType] = {}; - this.runFnRegistry[taskType][modelType] = runFn; + this.runFnRegistry[taskType][modelProvider] = runFn; } - jobAsRunFn(runtype: string, modelType: ModelProviderEnum) { + jobAsRunFn(runtype: string, modelType: string) { const fn = this.runFnRegistry[runtype]?.[modelType]; return async (task: JobQueueTask, input: Input) => { const queue = this.queues.get(modelType)!; @@ -69,16 +68,16 @@ export class ProviderRegistry { }; } - getDirectRunFn(taskType: string, modelType: ModelProviderEnum) { + getDirectRunFn(taskType: string, modelType: string) { return this.runFnRegistry[taskType]?.[modelType]; } - queues: Map> = new Map(); - registerQueue(modelType: ModelProviderEnum, jobQueue: JobQueue) { + queues: Map> = new Map(); + registerQueue(modelType: string, jobQueue: JobQueue) { this.queues.set(modelType, jobQueue); } - getQueue(modelType: ModelProviderEnum) { + getQueue(modelType: string) { return this.queues.get(modelType); } diff --git a/packages/ai/src/task/DownloadModelTask.ts b/packages/ai/src/task/DownloadModelTask.ts index a21be84..5be5dbd 100644 --- a/packages/ai/src/task/DownloadModelTask.ts +++ b/packages/ai/src/task/DownloadModelTask.ts @@ -16,8 +16,7 @@ import { TaskOutput, JobQueueTaskConfig, } from "ellmers-core"; -import { ModelUseCaseEnum } from "../model/Model"; -import { findModelByName } from "../model/InMemoryStorage"; +import { getGlobalModelRepository } from "../model/ModelRegistry"; import { JobQueueLlmTask } from "./base/JobQueueLlmTask"; export type DownloadModelTaskInput = CreateMappedType; @@ -81,28 +80,28 @@ export class DownloadModelTask extends JobQueueLlmTask { super(config); } runSyncOnly(): TaskOutput { - const model = findModelByName(this.runInputData.model); + const model = getGlobalModelRepository().findByName(this.runInputData.model); if (model) { - model.useCase.forEach((useCase) => { - // @ts-expect-error -- we really can use this an index - this.runOutputData[String(useCase).toLowerCase()] = model.name; + const tasks = getGlobalModelRepository().findTasksByModel(model.name); + tasks.forEach((task) => { + // this.runOutputData[String(task).toLowerCase()] = model.name; }); this.runOutputData.model = model.name; - this.runOutputData.dimensions = model.dimensions!; - this.runOutputData.normalize = model.normalize; - if (model.useCase.includes(ModelUseCaseEnum.TEXT_EMBEDDING)) { + this.runOutputData.dimensions = model.usingDimensions!; + this.runOutputData.normalize = model.normalize!; + if (tasks.includes("TextEmbeddingTask")) { this.runOutputData.text_embedding_model = model.name; } - if (model.useCase.includes(ModelUseCaseEnum.TEXT_GENERATION)) { + if (tasks.includes("TextGenerationTask")) { this.runOutputData.text_generation_model = model.name; } - if (model.useCase.includes(ModelUseCaseEnum.TEXT_SUMMARIZATION)) { + if (tasks.includes("TextSummaryTask")) { this.runOutputData.text_summarization_model = model.name; } - if (model.useCase.includes(ModelUseCaseEnum.TEXT_QUESTION_ANSWERING)) { + if (tasks.includes("TextQuestionAnswerTask")) { this.runOutputData.text_question_answering_model = model.name; } - if (model.useCase.includes(ModelUseCaseEnum.TEXT_TRANSLATION)) { + if (tasks.includes("TextTranslationTask")) { this.runOutputData.text_translation_model = model.name; } } diff --git a/packages/ai/src/task/base/JobQueueLlmTask.ts b/packages/ai/src/task/base/JobQueueLlmTask.ts index 3e52088..86f5792 100644 --- a/packages/ai/src/task/base/JobQueueLlmTask.ts +++ b/packages/ai/src/task/base/JobQueueLlmTask.ts @@ -10,8 +10,8 @@ */ import { JobQueueTask, JobQueueTaskConfig, type TaskOutput } from "ellmers-core"; -import { findModelByName } from "../../model/InMemoryStorage"; import { getProviderRegistry } from "../../provider/ProviderRegistry"; +import { getGlobalModelRepository } from "../../model/ModelRegistry"; export class JobQueueLlmTask extends JobQueueTask { static readonly type: string = "JobQueueLlmTask"; @@ -35,9 +35,12 @@ export class JobQueueLlmTask extends JobQueueTask { const ProviderRegistry = getProviderRegistry(); const modelname = this.runInputData["model"]; if (!modelname) throw new Error("JobQueueTaskTask: No model name found"); - const model = findModelByName(modelname); - if (!model) throw new Error("JobQueueTaskTask: No model found"); - const runFn = ProviderRegistry.jobAsRunFn(runtype, model.type); + const model = getGlobalModelRepository().findByName(modelname); + + if (!model) { + throw new Error(`JobQueueTaskTask: No model ${modelname} found ${modelname}`); + } + const runFn = ProviderRegistry.jobAsRunFn(runtype, model.provider); if (!runFn) throw new Error("JobQueueTaskTask: No run function found for " + runtype); results = await runFn(this, this.runInputData); } catch (err) { diff --git a/packages/core/src/storage/base/KVRepository.ts b/packages/core/src/storage/base/KVRepository.ts index 84631f9..b59fef1 100644 --- a/packages/core/src/storage/base/KVRepository.ts +++ b/packages/core/src/storage/base/KVRepository.ts @@ -47,7 +47,7 @@ export abstract class KVRepository< Value extends Record = DefaultValueType, PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, - Combined extends Key & Value = Key & Value + Combined extends Record = Key & Value > { // KV repository event emitter private events = new EventEmitter(); @@ -88,14 +88,15 @@ export abstract class KVRepository< ) { this.primaryKeySchema = primaryKeySchema; this.valueSchema = valueSchema; - if (Object.keys(primaryKeySchema).length === 1) { + if (this.primaryKeyColumns().length === 1) { this.primaryKeyIndex = this.primaryKeyColumns()[0] as string; } - if (Object.keys(valueSchema).length === 1) { + if (this.valueColumns().length === 1) { this.valueIndex = this.valueColumns()[0] as string; } - if (!searchable.includes(this.primaryKeyColumns()[0])) { - searchable.push(this.primaryKeyColumns()[0]); + const firstKeyPart = this.primaryKeyColumns()[0] as keyof Combined; + if (!searchable.includes(firstKeyPart)) { + searchable.push(firstKeyPart); } this.searchable = searchable; @@ -162,12 +163,12 @@ export abstract class KVRepository< /** * Abstract method to be implemented by concrete repositories to search for key-value pairs - * based on a partial key. + * based on a partial key or value. * - * @param key - Partial key to search for + * @param key - Partial key or value to search for * @returns Promise resolving to an array of combined key-value objects or undefined if not found */ - public abstract search(key: Partial): Promise; + public abstract search(key: Partial): Promise; /** * Retrieves both key and value as a combined object. @@ -223,10 +224,10 @@ export abstract class KVRepository< const value: Partial = {}; const key: Partial = {}; for (const k of primaryKeyNames) { - key[k] = obj[k]; + key[k] = obj[k as keyof Combined]; } for (const k of valueNames) { - value[k] = obj[k]; + value[k] = obj[k as keyof Combined]; } return { value: value as Value, key: key as Key }; diff --git a/packages/storage/src/browser/indexeddb/IndexedDbTaskGraphRepository.ts b/packages/storage/src/browser/indexeddb/IndexedDbTaskGraphRepository.ts index a0d7b43..a0932f5 100644 --- a/packages/storage/src/browser/indexeddb/IndexedDbTaskGraphRepository.ts +++ b/packages/storage/src/browser/indexeddb/IndexedDbTaskGraphRepository.ts @@ -5,14 +5,14 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { TaskGraphJson, TaskGraphRepository } from "ellmers-core"; +import { TaskGraphRepository } from "ellmers-core"; import { IndexedDbKVRepository } from "./base/IndexedDbKVRepository"; export class IndexedDbTaskGraphRepository extends TaskGraphRepository { - kvRepository: IndexedDbKVRepository; + kvRepository: IndexedDbKVRepository; public type = "IndexedDbTaskGraphRepository" as const; constructor() { super(); - this.kvRepository = new IndexedDbKVRepository("task_graphs"); + this.kvRepository = new IndexedDbKVRepository("task_graphs"); } } diff --git a/packages/storage/src/browser/indexeddb/IndexedDbTaskOutputRepository.ts b/packages/storage/src/browser/indexeddb/IndexedDbTaskOutputRepository.ts index f3b7b27..809a625 100644 --- a/packages/storage/src/browser/indexeddb/IndexedDbTaskOutputRepository.ts +++ b/packages/storage/src/browser/indexeddb/IndexedDbTaskOutputRepository.ts @@ -6,22 +6,26 @@ // ******************************************************************************* import { - TaskInput, - TaskOutput, + DefaultValueType, + TaskOutputPrimaryKey, TaskOutputPrimaryKeySchema, TaskOutputRepository, } from "ellmers-core"; import { IndexedDbKVRepository } from "./base/IndexedDbKVRepository"; export class IndexedDbTaskOutputRepository extends TaskOutputRepository { - kvRepository: IndexedDbKVRepository; + kvRepository: IndexedDbKVRepository< + TaskOutputPrimaryKey, + DefaultValueType, + typeof TaskOutputPrimaryKeySchema + >; public type = "IndexedDbTaskOutputRepository" as const; constructor() { super(); this.kvRepository = new IndexedDbKVRepository< - TaskInput, - TaskOutput, + TaskOutputPrimaryKey, + DefaultValueType, typeof TaskOutputPrimaryKeySchema - >("task_outputs"); + >("task_outputs", TaskOutputPrimaryKeySchema); } } diff --git a/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts b/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts index 7da6da7..5c5bb75 100644 --- a/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts +++ b/packages/storage/src/browser/indexeddb/base/IndexedDbKVRepository.ts @@ -37,7 +37,7 @@ export class IndexedDbKVRepository< Value extends Record = DefaultValueType, PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, - Combined extends Key & Value = Key & Value + Combined extends Record = Key & Value > extends KVRepository { /** Promise that resolves to the IndexedDB database instance */ private dbPromise: Promise; diff --git a/packages/storage/src/browser/inmemory/InMemoryModelRepository.ts b/packages/storage/src/browser/inmemory/InMemoryModelRepository.ts index 70a3990..373233c 100644 --- a/packages/storage/src/browser/inmemory/InMemoryModelRepository.ts +++ b/packages/storage/src/browser/inmemory/InMemoryModelRepository.ts @@ -5,15 +5,45 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { Model, ModelRepository } from "ellmers-ai"; +import { + ModelPrimaryKey, + ModelRepository, + Task2ModelDetail, + Task2ModelPrimaryKey, +} from "ellmers-ai"; import { InMemoryKVRepository } from "./base/InMemoryKVRepository"; -import { ModelPrimaryKeySchema } from "ellmers-ai"; +import { + ModelPrimaryKeySchema, + Task2ModelPrimaryKeySchema, + Task2ModelDetailSchema, +} from "ellmers-ai"; +import { DefaultValueType } from "ellmers-core"; export class InMemoryModelRepository extends ModelRepository { - kvRepository: InMemoryKVRepository; + modelKvRepository: InMemoryKVRepository< + ModelPrimaryKey, + DefaultValueType, + typeof ModelPrimaryKeySchema + >; + task2ModelKvRepository: InMemoryKVRepository< + Task2ModelPrimaryKey, + Task2ModelDetail, + typeof Task2ModelPrimaryKeySchema, + typeof Task2ModelDetailSchema + >; public type = "InMemoryModelRepository" as const; constructor() { super(); - this.kvRepository = new InMemoryKVRepository(); + this.modelKvRepository = new InMemoryKVRepository< + ModelPrimaryKey, + DefaultValueType, + typeof ModelPrimaryKeySchema + >(ModelPrimaryKeySchema); + this.task2ModelKvRepository = new InMemoryKVRepository< + Task2ModelPrimaryKey, + Task2ModelDetail, + typeof Task2ModelPrimaryKeySchema, + typeof Task2ModelDetailSchema + >(Task2ModelPrimaryKeySchema, Task2ModelDetailSchema); } } diff --git a/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts b/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts index 2775469..84efc1a 100644 --- a/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts +++ b/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts @@ -34,7 +34,7 @@ export class InMemoryKVRepository< Value extends Record = DefaultValueType, PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, - Combined extends Key & Value = Key & Value + Combined extends Record = Key & Value > extends KVRepository { /** Internal storage using a Map with fingerprint strings as keys */ values = new Map(); diff --git a/packages/storage/src/browser/inmemory/test/InMemoryModelRepository.test.ts b/packages/storage/src/browser/inmemory/test/InMemoryModelRepository.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/storage/src/bun/sqlite/SqliteModelRepository.ts b/packages/storage/src/bun/sqlite/SqliteModelRepository.ts index 98b842d..673f08d 100644 --- a/packages/storage/src/bun/sqlite/SqliteModelRepository.ts +++ b/packages/storage/src/bun/sqlite/SqliteModelRepository.ts @@ -5,18 +5,43 @@ // * Licensed under the Apache License, Version 2.0 (the "License"); * // ******************************************************************************* -import { Model, ModelRepository, ModelPrimaryKeySchema } from "ellmers-ai"; +import { + ModelRepository, + ModelPrimaryKeySchema, + ModelPrimaryKey, + Task2ModelDetailSchema, + Task2ModelPrimaryKey, + Task2ModelDetail, + Task2ModelPrimaryKeySchema, +} from "ellmers-ai"; import { SqliteKVRepository } from "./base/SqliteKVRepository"; +import { DefaultValueType } from "ellmers-core"; export class SqliteModelRepository extends ModelRepository { - kvRepository: SqliteKVRepository; public type = "SqliteModelRepository" as const; + modelKvRepository: SqliteKVRepository< + ModelPrimaryKey, + DefaultValueType, + typeof ModelPrimaryKeySchema + >; + task2ModelKvRepository: SqliteKVRepository< + Task2ModelPrimaryKey, + Task2ModelDetail, + typeof Task2ModelPrimaryKeySchema, + typeof Task2ModelDetailSchema + >; constructor(dbOrPath: string) { super(); - this.kvRepository = new SqliteKVRepository( - dbOrPath, - "aimodel", - ModelPrimaryKeySchema - ); + this.modelKvRepository = new SqliteKVRepository< + ModelPrimaryKey, + DefaultValueType, + typeof ModelPrimaryKeySchema + >(dbOrPath, "aimodel", ModelPrimaryKeySchema); + this.task2ModelKvRepository = new SqliteKVRepository< + Task2ModelPrimaryKey, + Task2ModelDetail, + typeof Task2ModelPrimaryKeySchema, + typeof Task2ModelDetailSchema + >(dbOrPath, "aitask2aimodel", Task2ModelPrimaryKeySchema, Task2ModelDetailSchema); } } diff --git a/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts b/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts index dd65432..81c2921 100644 --- a/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts +++ b/packages/storage/src/bun/sqlite/base/SqliteKVRepository.ts @@ -33,7 +33,7 @@ export class SqliteKVRepository< Value extends Record = DefaultValueType, PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, - Combined extends Key & Value = Key & Value + Combined extends Record = Key & Value > extends BaseSqlKVRepository { /** The SQLite database instance */ private db: Database; diff --git a/packages/storage/src/bun/sqlite/index.ts b/packages/storage/src/bun/sqlite/index.ts index b671f30..3b1728b 100644 --- a/packages/storage/src/bun/sqlite/index.ts +++ b/packages/storage/src/bun/sqlite/index.ts @@ -1,3 +1,4 @@ +export { getDatabase } from "../../util/db_sqlite"; export * from "./SqliteJobQueue"; export * from "./SqliteTaskGraphRepository"; export * from "./SqliteTaskOutputRepository"; diff --git a/packages/storage/src/node/postgres/base/PostgresKVRepository.ts b/packages/storage/src/node/postgres/base/PostgresKVRepository.ts index a516eed..f807a38 100644 --- a/packages/storage/src/node/postgres/base/PostgresKVRepository.ts +++ b/packages/storage/src/node/postgres/base/PostgresKVRepository.ts @@ -43,7 +43,7 @@ export class PostgresKVRepository< Value extends Record = DefaultValueType, PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, - Combined extends Key & Value = Key & Value + Combined extends Record = Key & Value > extends BaseSqlKVRepository { private pool: Pool; diff --git a/packages/storage/src/util/base/BaseSqlKVRepository.ts b/packages/storage/src/util/base/BaseSqlKVRepository.ts index 869e43a..6913617 100644 --- a/packages/storage/src/util/base/BaseSqlKVRepository.ts +++ b/packages/storage/src/util/base/BaseSqlKVRepository.ts @@ -34,7 +34,7 @@ export abstract class BaseSqlKVRepository< Value extends Record = DefaultValueType, PrimaryKeySchema extends BasePrimaryKeySchema = typeof DefaultPrimaryKeySchema, ValueSchema extends BaseValueSchema = typeof DefaultValueSchema, - Combined extends Key & Value = Key & Value + Combined extends Record = Key & Value > extends KVRepository { /** * Creates a new instance of BaseSqlKVRepository diff --git a/packages/test/package.json b/packages/test/package.json index 9cf2779..e20b518 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -7,7 +7,7 @@ "watch": "concurrently -c 'auto' 'bun:watch-*'", "watch-code": "bun build --watch --no-clear-screen --target=browser --sourcemap=external --external @mediapipe/tasks-text --external @huggingface/transformers --external ellmers-core --external ellmers-ai --external ellmers-ai-provider --external ellmers-storage --outdir ./dist/ ./src/index.ts", "watch-types": "tsc --watch --preserveWatchOutput", - "build": "bun run build-clean && bun run build-types && bun run build-hf-transformers && bun run build-tf-mediapipe", + "build": "bun run build-clean && bun run build-types && bun run build-code", "build-clean": "rm -fr dist/* tsconfig.tsbuildinfo", "build-code": "bun build --target=browser --sourcemap=external --external @mediapipe/tasks-text --external @mediapipe/tasks-text --external @huggingface/transformers --external ellmers-core --external ellmers-ai --external ellmers-ai-provider --external ellmers-storage --outdir ./dist/ ./src/index.ts", "build-types": "tsc", diff --git a/packages/test/src/index.ts b/packages/test/src/index.ts index a0a4d19..27d9f56 100644 --- a/packages/test/src/index.ts +++ b/packages/test/src/index.ts @@ -1,6 +1,12 @@ -import { getProviderRegistry, ModelProcessorEnum } from "ellmers-ai"; -import { registerHuggingfaceLocalTasks } from "ellmers-ai-provider/hf-transformers/browser"; -import { registerMediaPipeTfJsLocalTasks } from "ellmers-ai-provider/tf-mediapipe/browser"; +import { getProviderRegistry } from "ellmers-ai"; +import { + LOCAL_ONNX_TRANSFORMERJS, + registerHuggingfaceLocalTasks, +} from "ellmers-ai-provider/hf-transformers/browser"; +import { + MEDIA_PIPE_TFJS_MODEL, + registerMediaPipeTfJsLocalTasks, +} from "ellmers-ai-provider/tf-mediapipe/browser"; import { ConcurrencyLimiter, TaskInput, TaskOutput } from "ellmers-core"; import { InMemoryJobQueue } from "ellmers-storage/inmemory"; @@ -15,7 +21,7 @@ export async function registerHuggingfaceLocalTasksInMemory() { new ConcurrencyLimiter(1, 10), 10 ); - ProviderRegistry.registerQueue(ModelProcessorEnum.LOCAL_ONNX_TRANSFORMERJS, jobQueue); + ProviderRegistry.registerQueue(LOCAL_ONNX_TRANSFORMERJS, jobQueue); jobQueue.start(); } @@ -27,6 +33,6 @@ export async function registerMediaPipeTfJsLocalInMemory() { new ConcurrencyLimiter(1, 10), 10 ); - ProviderRegistry.registerQueue(ModelProcessorEnum.MEDIA_PIPE_TFJS_MODEL, jobQueue); + ProviderRegistry.registerQueue(MEDIA_PIPE_TFJS_MODEL, jobQueue); jobQueue.start(); } diff --git a/packages/test/src/sample/MediaPipeModelSamples.ts b/packages/test/src/sample/MediaPipeModelSamples.ts index 6c04223..a44ed5f 100644 --- a/packages/test/src/sample/MediaPipeModelSamples.ts +++ b/packages/test/src/sample/MediaPipeModelSamples.ts @@ -1,16 +1,49 @@ -import { ModelUseCaseEnum } from "ellmers-ai"; -import { MediaPipeTfJsModel } from "../../../ai-provider/src/tf-mediapipe/model/MediaPipeModel"; +import { MEDIA_PIPE_TFJS_MODEL } from "ellmers-ai-provider/tf-mediapipe/browser"; +import { getGlobalModelRepository, Model } from "ellmers-ai"; -export const universal_sentence_encoder = new MediaPipeTfJsModel( - "Universal Sentence Encoder", - [ModelUseCaseEnum.TEXT_EMBEDDING], - "https://storage.googleapis.com/mediapipe-tasks/text_embedder/universal_sentence_encoder.tflite", - { dimensions: 100, browserOnly: true } -); +function addMediaPipeModel(info: Partial, tasks: string[]) { + const name = "MEDIAPIPE " + info.name; -export const kerasSdTextEncoder = new MediaPipeTfJsModel( - "keras-sd/text-encoder-tflite", - [ModelUseCaseEnum.TEXT_EMBEDDING], - "https://huggingface.co/keras-sd/text-encoder-tflite/resolve/main/text_encoder.tflite?download=true", - { dimensions: 100, browserOnly: true } -); + const model = Object.assign( + { + provider: MEDIA_PIPE_TFJS_MODEL, + quantization: null, + normalize: true, + contextWindow: 4096, + availableOnBrowser: true, + availableOnServer: false, + parameters: null, + languageStyle: null, + usingDimensions: info.nativeDimensions ?? null, + }, + info, + { name } + ) as Model; + + getGlobalModelRepository().addModel(model); + tasks.forEach((task) => { + getGlobalModelRepository().connectTaskToModel(task, name); + }); +} + +export function registerMediaPipeTfJsLocalModels() { + addMediaPipeModel( + { + name: "Universal Sentence Encoder", + pipeline: "text_embedder", + nativeDimensions: 100, + url: "https://storage.googleapis.com/mediapipe-tasks/text_embedder/universal_sentence_encoder.tflite", + }, + ["TextEmbeddingTask"] + ); + + addMediaPipeModel( + { + name: "Text Encoder", + pipeline: "text_embedder", + nativeDimensions: 100, + url: "https://huggingface.co/keras-sd/text-encoder-tflite/resolve/main/text_encoder.tflite?download=true", + }, + ["TextEmbeddingTask"] + ); +} diff --git a/packages/test/src/sample/ONNXModelSamples.ts b/packages/test/src/sample/ONNXModelSamples.ts index 3adc5b5..b88226e 100644 --- a/packages/test/src/sample/ONNXModelSamples.ts +++ b/packages/test/src/sample/ONNXModelSamples.ts @@ -1,126 +1,178 @@ -import { DATA_TYPES, ONNXTransformerJsModel } from "ellmers-ai-provider/hf-transformers/browser"; -import { ModelUseCaseEnum } from "ellmers-ai"; - -export const supabaseGteSmall = new ONNXTransformerJsModel( - "Supabase/gte-small", - [ModelUseCaseEnum.TEXT_EMBEDDING], - "feature-extraction", - { dimensions: 384 } -); - -export const baaiBgeBaseEnV15 = new ONNXTransformerJsModel( - "Xenova/bge-base-en-v1.5", - [ModelUseCaseEnum.TEXT_EMBEDDING], - "feature-extraction", - { dimensions: 768 } -); - -export const xenovaMiniL6v2 = new ONNXTransformerJsModel( - "Xenova/all-MiniLM-L6-v2", - [ModelUseCaseEnum.TEXT_EMBEDDING], - "feature-extraction", - { dimensions: 384 } -); - -export const whereIsAIUAELargeV1 = new ONNXTransformerJsModel( - "WhereIsAI/UAE-Large-V1", - [ModelUseCaseEnum.TEXT_EMBEDDING], - "feature-extraction", - { dimensions: 1024 } -); - -export const baaiBgeSmallEnV15 = new ONNXTransformerJsModel( - "Xenova/bge-small-en-v1.5", - [ModelUseCaseEnum.TEXT_EMBEDDING], - "feature-extraction", - { dimensions: 384 } -); - -export const xenovaDistilbert = new ONNXTransformerJsModel( - "Xenova/distilbert-base-uncased-distilled-squad", - [ModelUseCaseEnum.TEXT_QUESTION_ANSWERING], - "question-answering" -); - -export const xenovaDistilbertMnli = new ONNXTransformerJsModel( - "Xenova/distilbert-base-uncased-mnli", - [ModelUseCaseEnum.TEXT_CLASSIFICATION], - "zero-shot-classification" -); - -export const modernBertBase = new ONNXTransformerJsModel( - "answerdotai/ModernBERT-base", - [ModelUseCaseEnum.TEXT_CLASSIFICATION], - "fill-mask" -); - -export const stentancetransformerMultiQaMpnetBaseDotV1 = new ONNXTransformerJsModel( - "Xenova/multi-qa-mpnet-base-dot-v1", - [ModelUseCaseEnum.TEXT_EMBEDDING], - "feature-extraction", - { dimensions: 768 } -); - -export const gpt2 = new ONNXTransformerJsModel( - "Xenova/gpt2", - [ModelUseCaseEnum.TEXT_GENERATION], - "text-generation" -); - -export const distillgpt2 = new ONNXTransformerJsModel( - "Xenova/distilgpt2", - [ModelUseCaseEnum.TEXT_GENERATION], - "text-generation" -); - -export const flanT5small = new ONNXTransformerJsModel( - "Xenova/flan-t5-small", - [ModelUseCaseEnum.TEXT_GENERATION], - "text2text-generation" -); - -export const flanT5p786m = new ONNXTransformerJsModel( - "Xenova/LaMini-Flan-T5-783M", - [ModelUseCaseEnum.TEXT_GENERATION, ModelUseCaseEnum.TEXT_REWRITING], - "text2text-generation" -); - -export const text_summarization = new ONNXTransformerJsModel( - "Falconsai/text_summarization", - [ModelUseCaseEnum.TEXT_SUMMARIZATION], - "summarization", - { dtype: DATA_TYPES.fp32 } -); - -// export const distilbartCnn = new ONNXTransformerJsModel( -// "Xenova/distilbart-cnn-6-6", -// [ModelUseCaseEnum.TEXT_SUMMARIZATION], -// "summarization" -// ); - -// export const bartLargeCnn = new ONNXTransformerJsModel( -// "Xenova/bart-large-cnn", -// [ModelUseCaseEnum.TEXT_SUMMARIZATION], -// "summarization" -// ); - -export const nllb200distilled600m = new ONNXTransformerJsModel( - "Xenova/nllb-200-distilled-600M", - [ModelUseCaseEnum.TEXT_TRANSLATION], - "translation", - { languageStyle: "FLORES-200" } -); - -export const m2m100_418M = new ONNXTransformerJsModel( - "Xenova/m2m100_418M", - [ModelUseCaseEnum.TEXT_TRANSLATION], - "translation", - { languageStyle: "ISO-639" } -); - -export const mbartLarge50many2manyMmt = new ONNXTransformerJsModel( - "Xenova/mbart-large-50-many-to-many-mmt", - [ModelUseCaseEnum.TEXT_TRANSLATION], - "translation", - { languageStyle: "ISO-639_ISO-3166-1-alpha-2" } -); +import { + LOCAL_ONNX_TRANSFORMERJS, + QUANTIZATION_DATA_TYPES, +} from "ellmers-ai-provider/hf-transformers/browser"; +import { getGlobalModelRepository, Model } from "ellmers-ai"; + +function addONNXModel(info: Partial, tasks: string[]) { + const name = info.name + ? info.name + : "ONNX " + info.url + " " + (info.quantization ?? QUANTIZATION_DATA_TYPES.q8); + + const model = Object.assign( + { + provider: LOCAL_ONNX_TRANSFORMERJS, + quantization: QUANTIZATION_DATA_TYPES.q8, + normalize: true, + contextWindow: 4096, + availableOnBrowser: true, + availableOnServer: true, + parameters: null, + languageStyle: null, + usingDimensions: info.nativeDimensions ?? null, + }, + info, + { name } + ) as Model; + + getGlobalModelRepository().addModel(model); + tasks.forEach((task) => { + getGlobalModelRepository().connectTaskToModel(task, name); + }); +} + +export function registerHuggingfaceLocalModels() { + addONNXModel( + { + pipeline: "feature-extraction", + nativeDimensions: 384, + url: "Supabase/gte-small", + }, + ["TextEmbeddingTask"] + ); + + addONNXModel( + { + pipeline: "feature-extraction", + nativeDimensions: 768, + url: "Xenova/bge-base-en-v1.5", + }, + ["TextEmbeddingTask"] + ); + + addONNXModel( + { + pipeline: "feature-extraction", + nativeDimensions: 384, + url: "Xenova/all-MiniLM-L6-v2", + }, + ["TextEmbeddingTask"] + ); + + addONNXModel( + { + pipeline: "feature-extraction", + nativeDimensions: 1024, + url: "WhereIsAI/UAE-Large-V1", + }, + ["TextEmbeddingTask"] + ); + + addONNXModel( + { + pipeline: "feature-extraction", + nativeDimensions: 384, + url: "Xenova/bge-small-en-v1.5", + }, + ["TextEmbeddingTask"] + ); + addONNXModel( + { + pipeline: "question-answering", + url: "Xenova/distilbert-base-uncased-distilled-squad", + }, + ["TextQuestionAnsweringTask"] + ); + + addONNXModel( + { + pipeline: "zero-shot-classification", + url: "Xenova/distilbert-base-uncased-mnli", + }, + ["TextClassificationTask"] + ); + + addONNXModel( + { + pipeline: "fill-mask", + url: "answerdotai/ModernBERT-base", + }, + ["TextClassificationTask"] + ); + + addONNXModel( + { + pipeline: "feature-extraction", + nativeDimensions: 768, + url: "Xenova/multi-qa-mpnet-base-dot-v1", + }, + ["TextEmbeddingTask"] + ); + + addONNXModel( + { + pipeline: "text-generation", + url: "Xenova/gpt2", + }, + ["TextGenerationTask"] + ); + + addONNXModel( + { + pipeline: "text-generation", + url: "Xenova/distilgpt2", + }, + ["TextGenerationTask"] + ); + + addONNXModel( + { + pipeline: "text2text-generation", + url: "Xenova/flan-t5-small", + }, + ["TextGenerationTask"] + ); + + addONNXModel( + { + pipeline: "text2text-generation", + url: "Xenova/LaMini-Flan-T5-783M", + }, + ["TextGenerationTask"] + ); + + addONNXModel( + { + pipeline: "summarization", + url: "Falconsai/text_summarization", + }, + ["TextSummaryTask"] + ); + + addONNXModel( + { + pipeline: "translation", + url: "Xenova/nllb-200-distilled-600M", + languageStyle: "FLORES-200", + }, + ["TextTranslationTask"] + ); + + addONNXModel( + { + pipeline: "translation", + url: "Xenova/m2m100_418M", + languageStyle: "ISO-639", + }, + ["TextTranslationTask"] + ); + + addONNXModel( + { + pipeline: "translation", + url: "Xenova/mbart-large-50-many-to-many-mmt", + languageStyle: "ISO-639_ISO-3166-1-alpha-2", + }, + ["TextTranslationTask"] + ); +} From addafa7246d3140122fdb070f7ac5e66ee175694 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Fri, 17 Jan 2025 14:17:43 -0800 Subject: [PATCH 08/12] chore: update packages --- bun.lockb | Bin 233284 -> 236524 bytes examples/web/package.json | 16 ++++++++-------- package.json | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bun.lockb b/bun.lockb index 82c165b9a0b17f9930346592118d6c11b485dc96..153d251b7b2e7fe5e67b9d30caebd513ca1e39f6 100755 GIT binary patch delta 23115 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 K%zc?+?f(D>HJ>E_ diff --git a/examples/web/package.json b/examples/web/package.json index 59844d3..738d4f7 100644 --- a/examples/web/package.json +++ b/examples/web/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@xyflow/react": "^12.3.6", + "@xyflow/react": "^12.4.1", "react": "^19.0.0", "react-dom": "^19.0.0", "@uiw/react-codemirror": "^4.23.7", @@ -28,17 +28,17 @@ "ellmers-test": "workspace:packages/test" }, "devDependencies": { - "@types/react": "^19.0.4", - "@types/react-dom": "^19.0.2", - "@typescript-eslint/eslint-plugin": "^8.19.1", - "@typescript-eslint/parser": "^8.19.1", + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", + "@typescript-eslint/eslint-plugin": "^8.20.0", + "@typescript-eslint/parser": "^8.20.0", "@vitejs/plugin-react": "^4.3.4", - "eslint": "^9.17.0", + "eslint": "^9.18.0", "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-react-refresh": "^0.4.16", + "eslint-plugin-react-refresh": "^0.4.18", "vite": "^6.0.7", "tailwindcss": "3.4.17", - "postcss": "8.4.49", + "postcss": "8.5.1", "autoprefixer": "10.4.20" }, "engines": { diff --git a/package.json b/package.json index ed6f83d..c7e5dac 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ ], "scripts": { "build": "bun run build:packages && bun run build:examples", - "build:packages": "bun run build:core && bun run build:storage && bun run build:task && bun run build:ai && bun run build:ai-provider", + "build:packages": "bun run build:core && bun run build:ai && bun run build:storage && bun run build:task && bun run build:ai-provider && bun run build:test", "build:core": "cd packages/core && bun run build", "build:ai": "cd packages/ai && bun run build", "build:ai-provider": "cd packages/ai-provider && bun run build", @@ -28,12 +28,12 @@ }, "dependencies": { "@mediapipe/tasks-text": "^0.10.20", - "@huggingface/transformers": "^3.2.4", + "@huggingface/transformers": "^3.3.1", "@sroussey/typescript-graph": "^0.3.14", "@types/better-sqlite3": "^7.6.12", "@types/pg": "^8.11.10", "better-sqlite3": "^9.4.3", - "bun-types": "^1.1.43", + "bun-types": "^1.1.45", "chalk": "^5.4.1", "commander": "=11.1.0", "eventemitter3": "^5.0.1", @@ -42,7 +42,7 @@ "pg": "^8.13.1", "postgres": "^3.4.5", "rxjs": "^7.8.1", - "storybook": "^8.4.7", + "storybook": "^8.5.0", "uuid": "^9.0.1" }, "devDependencies": { From 2b15d0980c3d317f05135cea97653919949a2c3c Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Fri, 17 Jan 2025 15:02:02 -0800 Subject: [PATCH 09/12] nit: version of bun that does not fail KV tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7e5dac..4dfe82a 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "typescript": "^5.7.3" }, "engines": { - "bun": "^1.1.43" + "bun": "^1.1.45" }, "trustedDependencies": [ "better-sqlite3", From cc4120b2f5764f5afd39678b11cb8a3d36070cd9 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Fri, 17 Jan 2025 15:03:14 -0800 Subject: [PATCH 10/12] refactor: rename runSyncOnly to runReactive The reactiveness is key, not the fact that there is no async code, and we will need async code in reactive stuff soon. --- bun.lockb | Bin 236524 -> 236892 bytes docs/developers/02_architecture.md | 2 +- docs/developers/03_extending.md | 2 +- package.json | 3 ++- packages/ai/src/task/DocumentSplitterTask.ts | 2 +- packages/ai/src/task/DownloadModelTask.ts | 2 +- packages/ai/src/task/SimilarityTask.ts | 2 +- packages/ai/src/task/base/JobQueueLlmTask.ts | 4 ++-- packages/core/src/task/DebugLogTask.ts | 2 +- packages/core/src/task/LambdaTask.ts | 2 +- packages/core/src/task/base/ArrayTask.ts | 9 ++++----- packages/core/src/task/base/Task.ts | 6 +++--- .../core/src/task/base/TaskGraphRunner.ts | 9 +++------ packages/core/src/task/test/Task.test.ts | 8 ++++---- packages/core/src/task/test/TaskGraph.test.ts | 2 +- .../src/task/test/TaskGraphRunner.test.ts | 12 ++++++------ .../src/task/test/TaskSubGraphRunner.test.ts | 6 +++--- .../test/InMemoryTaskGraphRepository.test.ts | 2 +- .../test/SqliteTaskGraphRepository.test.ts | 2 +- .../test/FileTaskGraphRepository.test.ts | 2 +- packages/task/src/task/JavaScriptTask.ts | 2 +- tsconfig.json | 2 ++ 22 files changed, 41 insertions(+), 42 deletions(-) diff --git a/bun.lockb b/bun.lockb index 153d251b7b2e7fe5e67b9d30caebd513ca1e39f6..6fd8d44242bcf036e5c969c362323f8e1c9e7f73 100755 GIT binary patch delta 41528 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 TaskBase style TaskBase type:abstract,stroke-dasharray: 5 5 diff --git a/docs/developers/03_extending.md b/docs/developers/03_extending.md index ad7ec18..128563a 100644 --- a/docs/developers/03_extending.md +++ b/docs/developers/03_extending.md @@ -147,4 +147,4 @@ Compound Tasks are not cached (though any or all of their children may be). ## Reactive Task UIs -Tasks can be reactive at a certain level. This means that they can be triggered by changes in the data they depend on, without "running" the expensive job based task runs. This is useful for a UI node editor. For example, you change a color in one task and it is propagated downstream without incurring costs for re-running the entire graph. It is like a spreadsheet where changing a cell can trigger a recalculation of other cells. This is implemented via a `runSyncOnly()` method that is called when the data changes. Typically, the `run()` will call `runSyncOnly()` on itself at the end of the method. +Tasks can be reactive at a certain level. This means that they can be triggered by changes in the data they depend on, without "running" the expensive job based task runs. This is useful for a UI node editor. For example, you change a color in one task and it is propagated downstream without incurring costs for re-running the entire graph. It is like a spreadsheet where changing a cell can trigger a recalculation of other cells. This is implemented via a `runReactive()` method that is called when the data changes. Typically, the `run()` will call `runReactive()` on itself at the end of the method. diff --git a/package.json b/package.json index 4dfe82a..521ceb8 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "test": "jest" }, "dependencies": { - "@mediapipe/tasks-text": "^0.10.20", "@huggingface/transformers": "^3.3.1", + "@mediapipe/tasks-text": "^0.10.20", "@sroussey/typescript-graph": "^0.3.14", "@types/better-sqlite3": "^7.6.12", "@types/pg": "^8.11.10", @@ -41,6 +41,7 @@ "nanoid": "^5.0.9", "pg": "^8.13.1", "postgres": "^3.4.5", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "storybook": "^8.5.0", "uuid": "^9.0.1" diff --git a/packages/ai/src/task/DocumentSplitterTask.ts b/packages/ai/src/task/DocumentSplitterTask.ts index 91378a4..35df661 100644 --- a/packages/ai/src/task/DocumentSplitterTask.ts +++ b/packages/ai/src/task/DocumentSplitterTask.ts @@ -63,7 +63,7 @@ export class DocumentSplitterTask extends SingleTask { } } - runSyncOnly(): DocumentSplitterTaskOutput { + runReactive(): DocumentSplitterTaskOutput { return { texts: this.flattenFragmentsToTexts(this.runInputData.file) }; } } diff --git a/packages/ai/src/task/DownloadModelTask.ts b/packages/ai/src/task/DownloadModelTask.ts index 5be5dbd..016c580 100644 --- a/packages/ai/src/task/DownloadModelTask.ts +++ b/packages/ai/src/task/DownloadModelTask.ts @@ -79,7 +79,7 @@ export class DownloadModelTask extends JobQueueLlmTask { constructor(config: JobQueueTaskConfig & { input?: DownloadModelTaskInput } = {}) { super(config); } - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { const model = getGlobalModelRepository().findByName(this.runInputData.model); if (model) { const tasks = getGlobalModelRepository().findTasksByModel(model.name); diff --git a/packages/ai/src/task/SimilarityTask.ts b/packages/ai/src/task/SimilarityTask.ts index 747556f..7e88288 100644 --- a/packages/ai/src/task/SimilarityTask.ts +++ b/packages/ai/src/task/SimilarityTask.ts @@ -115,7 +115,7 @@ export class SimilarityTask extends SingleTask { } } - runSyncOnly() { + runReactive() { const query = this.runInputData.query as ElVector; let similarities = []; const fns = { cosine_similarity }; diff --git a/packages/ai/src/task/base/JobQueueLlmTask.ts b/packages/ai/src/task/base/JobQueueLlmTask.ts index 86f5792..1db0bb4 100644 --- a/packages/ai/src/task/base/JobQueueLlmTask.ts +++ b/packages/ai/src/task/base/JobQueueLlmTask.ts @@ -50,10 +50,10 @@ export class JobQueueLlmTask extends JobQueueTask { } this.emit("complete"); this.runOutputData = results ?? {}; - this.runOutputData = this.runSyncOnly(); + this.runOutputData = this.runReactive(); return this.runOutputData; } - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { return this.runOutputData ?? {}; } } diff --git a/packages/core/src/task/DebugLogTask.ts b/packages/core/src/task/DebugLogTask.ts index 5c3e296..70dc294 100644 --- a/packages/core/src/task/DebugLogTask.ts +++ b/packages/core/src/task/DebugLogTask.ts @@ -32,7 +32,7 @@ export class DebugLogTask extends OutputTask { }, ] as const; public static outputs = [{ id: "output", name: "Output", valueType: "any" }] as const; - runSyncOnly() { + runReactive() { const level = this.runInputData.level || "log"; if (level == "dir") { console.dir(this.runInputData.message, { depth: null }); diff --git a/packages/core/src/task/LambdaTask.ts b/packages/core/src/task/LambdaTask.ts index 57d0883..de1a7bf 100644 --- a/packages/core/src/task/LambdaTask.ts +++ b/packages/core/src/task/LambdaTask.ts @@ -43,7 +43,7 @@ export class LambdaTask extends SingleTask { constructor(config: TaskConfig & { input?: LambdaTaskInput } = {}) { super(config); } - runSyncOnly() { + runReactive() { if (!this.runInputData.fn) { throw new Error("No runner provided"); } diff --git a/packages/core/src/task/base/ArrayTask.ts b/packages/core/src/task/base/ArrayTask.ts index 8286be6..2579951 100644 --- a/packages/core/src/task/base/ArrayTask.ts +++ b/packages/core/src/task/base/ArrayTask.ts @@ -104,8 +104,7 @@ function generateCombinations(input: T, inputMakeArray: (ke // Move to the next combination of indices for (let i = indices.length - 1; i >= 0; i--) { if (++indices[i] < arraysToCombine[i].length) break; // Increment current index if possible - if (i === 0) - done = true; // All combinations have been generated + if (i === 0) done = true; // All combinations have been generated else indices[i] = 0; // Reset current index and move to the next position } } @@ -126,7 +125,7 @@ function generateCombinations(input: T, inputMakeArray: (ke export function arrayTaskFactory< PluralInputType extends TaskInput = TaskInput, - PluralOutputType extends TaskOutput = TaskOutput, + PluralOutputType extends TaskOutput = TaskOutput >( taskClass: typeof SingleTask | typeof CompoundTask, inputMakeArray: Array, @@ -175,8 +174,8 @@ export function arrayTaskFactory< return this; } - runSyncOnly(): PluralOutputType { - const runDataOut = super.runSyncOnly(); + runReactive(): PluralOutputType { + const runDataOut = super.runReactive(); this.runOutputData = collectPropertyValues( runDataOut.outputs ) as PluralOutputType; diff --git a/packages/core/src/task/base/Task.ts b/packages/core/src/task/base/Task.ts index 1b9bf88..cd49021 100644 --- a/packages/core/src/task/base/Task.ts +++ b/packages/core/src/task/base/Task.ts @@ -272,12 +272,12 @@ export abstract class TaskBase { throw new Error("Invalid input data"); } this.emit("start"); - const result = this.runSyncOnly(); + const result = this.runReactive(); this.emit("complete"); this.runOutputData = result; return result; } - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { return this.runOutputData; } @@ -334,7 +334,7 @@ export class CompoundTask extends TaskBase implements ITaskCompound { this.emit("complete"); return this.runOutputData; } - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { const runner = new TaskGraphRunner(this.subGraph); this.runOutputData.outputs = runner.runGraphSyncOnly(); return this.runOutputData; diff --git a/packages/core/src/task/base/TaskGraphRunner.ts b/packages/core/src/task/base/TaskGraphRunner.ts index 512bd56..e7f6586 100644 --- a/packages/core/src/task/base/TaskGraphRunner.ts +++ b/packages/core/src/task/base/TaskGraphRunner.ts @@ -13,10 +13,7 @@ export class TaskGraphRunner { public layers: Map; public provenanceInput: Map; - constructor( - public dag: TaskGraph, - public repository?: TaskOutputRepository - ) { + constructor(public dag: TaskGraph, public repository?: TaskOutputRepository) { this.layers = new Map(); this.provenanceInput = new Map(); } @@ -99,7 +96,7 @@ export class TaskGraphRunner { task.emit("start"); task.emit("progress", 100, Object.values(results)[0]); task.runOutputData = results; - task.runSyncOnly(); + task.runReactive(); task.emit("complete"); } } @@ -140,7 +137,7 @@ export class TaskGraphRunner { for (const [_layerNumber, nodes] of this.layers.entries()) { results = nodes.map((node) => { this.copyInputFromEdgesToNode(node); - const results = node.runSyncOnly(); + const results = node.runReactive(); this.pushOutputFromNodeToEdges(node, results); return results; }); diff --git a/packages/core/src/task/test/Task.test.ts b/packages/core/src/task/test/Task.test.ts index 683f920..02a9418 100644 --- a/packages/core/src/task/test/Task.test.ts +++ b/packages/core/src/task/test/Task.test.ts @@ -41,7 +41,7 @@ class TestTask extends SingleTask { valueType: "text", }, ] as const; - runSyncOnly(): TestTaskOutput { + runReactive(): TestTaskOutput { return { all: false, key: this.runInputData.key, syncOnly: true }; } async run(): Promise { @@ -78,7 +78,7 @@ class TestCompoundTask extends CompoundTask { }, ] as const; static readonly type = "TestCompoundTask"; - runSyncOnly(): TestTaskOutput { + runReactive(): TestTaskOutput { this.runOutputData = { key: this.runInputData.key, all: false, syncOnly: true }; return this.runOutputData; } @@ -101,7 +101,7 @@ describe("Task", () => { it("should run the task synchronously", () => { const node = new TestTask(); - const output = node.runSyncOnly(); + const output = node.runReactive(); expect(output).toEqual({ key: "", syncOnly: true, all: false }); }); }); @@ -129,7 +129,7 @@ describe("Task", () => { it("should run the task synchronously", () => { const node = new TestCompoundTask({ input: { key: "value2" } }); - const output = node.runSyncOnly(); + const output = node.runReactive(); expect(output).toEqual({ key: "value2", syncOnly: true, all: false }); }); }); diff --git a/packages/core/src/task/test/TaskGraph.test.ts b/packages/core/src/task/test/TaskGraph.test.ts index 1371e03..02fd1b8 100644 --- a/packages/core/src/task/test/TaskGraph.test.ts +++ b/packages/core/src/task/test/TaskGraph.test.ts @@ -11,7 +11,7 @@ import { TaskGraph, DataFlow, serialGraph } from "../base/TaskGraph"; class TestTask extends SingleTask { static readonly type = "TestTask"; - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { return {}; } } diff --git a/packages/core/src/task/test/TaskGraphRunner.test.ts b/packages/core/src/task/test/TaskGraphRunner.test.ts index 42cbffe..a5a43c4 100644 --- a/packages/core/src/task/test/TaskGraphRunner.test.ts +++ b/packages/core/src/task/test/TaskGraphRunner.test.ts @@ -13,7 +13,7 @@ import { CreateMappedType } from "../base/TaskIOTypes"; class TestTask extends SingleTask { static readonly type = "TestTask"; - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { return {}; } } @@ -40,7 +40,7 @@ class TestSquareTask extends SingleTask { valueType: "number", }, ] as const; - runSyncOnly(): TestSquareTaskOutput { + runReactive(): TestSquareTaskOutput { return { output: this.runInputData.input * this.runInputData.input }; } } @@ -66,7 +66,7 @@ class TestDoubleTask extends SingleTask { valueType: "number", }, ] as const; - runSyncOnly(): TestDoubleTaskOutput { + runReactive(): TestDoubleTaskOutput { return { output: this.runInputData.input * 2 }; } } @@ -98,7 +98,7 @@ class TestAddTask extends SingleTask { valueType: "number", }, ] as const; - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { const input = this.runInputData; return { output: input.a + input.b }; } @@ -148,11 +148,11 @@ describe("TaskGraphRunner", () => { describe("runGraphSyncOnly", () => { it("should run nodes in each layer synchronously", () => { - const runSyncOnlySpy = spyOn(nodes[0], "runSyncOnly"); + const runReactiveSpy = spyOn(nodes[0], "runReactive"); runner.runGraphSyncOnly(); - expect(runSyncOnlySpy).toHaveBeenCalledTimes(1); + expect(runReactiveSpy).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/core/src/task/test/TaskSubGraphRunner.test.ts b/packages/core/src/task/test/TaskSubGraphRunner.test.ts index 3157d3a..b17c4bc 100644 --- a/packages/core/src/task/test/TaskSubGraphRunner.test.ts +++ b/packages/core/src/task/test/TaskSubGraphRunner.test.ts @@ -37,7 +37,7 @@ class TestSquareTask extends SingleTask { valueType: "number", }, ] as const; - runSyncOnly(): TestSquareTaskOutput { + runReactive(): TestSquareTaskOutput { return { output: this.runInputData.input * this.runInputData.input }; } } @@ -63,7 +63,7 @@ class TestDoubleTask extends SingleTask { valueType: "number", }, ] as const; - runSyncOnly(): TestDoubleTaskOutput { + runReactive(): TestDoubleTaskOutput { return { output: this.runInputData.input * 2 }; } } @@ -90,7 +90,7 @@ class TestAddTask extends SingleTask { valueType: "number", }, ] as const; - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { const inputs = Array.isArray(this.runInputData.input) ? this.runInputData.input : [this.runInputData.input ?? 0]; diff --git a/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts b/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts index 96c798d..f113a6e 100644 --- a/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts +++ b/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts @@ -11,7 +11,7 @@ import { InMemoryTaskGraphRepository } from "ellmers-storage/inmemory"; class TestTask extends SingleTask { static readonly type = "TestTask"; - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { return {}; } } diff --git a/packages/storage/src/bun/sqlite/test/SqliteTaskGraphRepository.test.ts b/packages/storage/src/bun/sqlite/test/SqliteTaskGraphRepository.test.ts index 2e6c705..989e97c 100644 --- a/packages/storage/src/bun/sqlite/test/SqliteTaskGraphRepository.test.ts +++ b/packages/storage/src/bun/sqlite/test/SqliteTaskGraphRepository.test.ts @@ -11,7 +11,7 @@ import { SingleTask, TaskOutput, DataFlow, TaskGraph, TaskRegistry } from "ellme class TestTask extends SingleTask { static readonly type = "TestTask"; - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { return {}; } } diff --git a/packages/storage/src/node/filesystem/test/FileTaskGraphRepository.test.ts b/packages/storage/src/node/filesystem/test/FileTaskGraphRepository.test.ts index 72e6741..6bdce3e 100644 --- a/packages/storage/src/node/filesystem/test/FileTaskGraphRepository.test.ts +++ b/packages/storage/src/node/filesystem/test/FileTaskGraphRepository.test.ts @@ -12,7 +12,7 @@ import { SingleTask, TaskOutput, TaskRegistry, DataFlow, TaskGraph } from "ellme class TestTask extends SingleTask { static readonly type = "TestTask"; - runSyncOnly(): TaskOutput { + runReactive(): TaskOutput { return {}; } } diff --git a/packages/task/src/task/JavaScriptTask.ts b/packages/task/src/task/JavaScriptTask.ts index 0fc23ad..2600004 100644 --- a/packages/task/src/task/JavaScriptTask.ts +++ b/packages/task/src/task/JavaScriptTask.ts @@ -46,7 +46,7 @@ export class JavaScriptTask extends SingleTask { constructor(config: TaskConfig & { input?: JavaScriptTaskInput } = {}) { super(config); } - runSyncOnly() { + runReactive() { if (this.runInputData.code) { try { const myInterpreter = new Interpreter(this.runInputData.code); diff --git a/tsconfig.json b/tsconfig.json index 376192d..2116cb3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,8 @@ "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "allowJs": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, "declaration": true, "emitDeclarationOnly": true, "declarationMap": true, From 2317a16f865a7ed6d0f523007877b2cd7aeee15d Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Fri, 17 Jan 2025 16:08:39 -0800 Subject: [PATCH 11/12] refactor: runReactive are now async --- packages/ai/src/task/DocumentSplitterTask.ts | 2 +- packages/ai/src/task/DownloadModelTask.ts | 2 +- packages/ai/src/task/SimilarityTask.ts | 2 +- packages/ai/src/task/base/JobQueueLlmTask.ts | 4 +-- packages/core/src/task/DebugLogTask.ts | 2 +- packages/core/src/task/LambdaTask.ts | 2 +- packages/core/src/task/base/ArrayTask.ts | 4 +-- packages/core/src/task/base/Task.ts | 8 ++--- .../core/src/task/base/TaskGraphRunner.ts | 23 +++++++------ packages/core/src/task/test/Task.test.ts | 32 +++++++++---------- packages/core/src/task/test/TaskGraph.test.ts | 2 +- .../src/task/test/TaskGraphRunner.test.ts | 14 ++++---- .../src/task/test/TaskSubGraphRunner.test.ts | 6 ++-- .../test/InMemoryTaskGraphRepository.test.ts | 2 +- .../test/SqliteTaskGraphRepository.test.ts | 2 +- .../test/FileTaskGraphRepository.test.ts | 2 +- packages/task/src/task/JavaScriptTask.ts | 2 +- 17 files changed, 57 insertions(+), 54 deletions(-) diff --git a/packages/ai/src/task/DocumentSplitterTask.ts b/packages/ai/src/task/DocumentSplitterTask.ts index 35df661..cd51b66 100644 --- a/packages/ai/src/task/DocumentSplitterTask.ts +++ b/packages/ai/src/task/DocumentSplitterTask.ts @@ -63,7 +63,7 @@ export class DocumentSplitterTask extends SingleTask { } } - runReactive(): DocumentSplitterTaskOutput { + async runReactive(): Promise { return { texts: this.flattenFragmentsToTexts(this.runInputData.file) }; } } diff --git a/packages/ai/src/task/DownloadModelTask.ts b/packages/ai/src/task/DownloadModelTask.ts index 016c580..17ca2aa 100644 --- a/packages/ai/src/task/DownloadModelTask.ts +++ b/packages/ai/src/task/DownloadModelTask.ts @@ -79,7 +79,7 @@ export class DownloadModelTask extends JobQueueLlmTask { constructor(config: JobQueueTaskConfig & { input?: DownloadModelTaskInput } = {}) { super(config); } - runReactive(): TaskOutput { + async runReactive(): Promise { const model = getGlobalModelRepository().findByName(this.runInputData.model); if (model) { const tasks = getGlobalModelRepository().findTasksByModel(model.name); diff --git a/packages/ai/src/task/SimilarityTask.ts b/packages/ai/src/task/SimilarityTask.ts index 7e88288..23bcb13 100644 --- a/packages/ai/src/task/SimilarityTask.ts +++ b/packages/ai/src/task/SimilarityTask.ts @@ -115,7 +115,7 @@ export class SimilarityTask extends SingleTask { } } - runReactive() { + async runReactive() { const query = this.runInputData.query as ElVector; let similarities = []; const fns = { cosine_similarity }; diff --git a/packages/ai/src/task/base/JobQueueLlmTask.ts b/packages/ai/src/task/base/JobQueueLlmTask.ts index 1db0bb4..f436414 100644 --- a/packages/ai/src/task/base/JobQueueLlmTask.ts +++ b/packages/ai/src/task/base/JobQueueLlmTask.ts @@ -50,10 +50,10 @@ export class JobQueueLlmTask extends JobQueueTask { } this.emit("complete"); this.runOutputData = results ?? {}; - this.runOutputData = this.runReactive(); + this.runOutputData = await this.runReactive(); return this.runOutputData; } - runReactive(): TaskOutput { + async runReactive(): Promise { return this.runOutputData ?? {}; } } diff --git a/packages/core/src/task/DebugLogTask.ts b/packages/core/src/task/DebugLogTask.ts index 70dc294..0d827a2 100644 --- a/packages/core/src/task/DebugLogTask.ts +++ b/packages/core/src/task/DebugLogTask.ts @@ -32,7 +32,7 @@ export class DebugLogTask extends OutputTask { }, ] as const; public static outputs = [{ id: "output", name: "Output", valueType: "any" }] as const; - runReactive() { + async runReactive() { const level = this.runInputData.level || "log"; if (level == "dir") { console.dir(this.runInputData.message, { depth: null }); diff --git a/packages/core/src/task/LambdaTask.ts b/packages/core/src/task/LambdaTask.ts index de1a7bf..7052b28 100644 --- a/packages/core/src/task/LambdaTask.ts +++ b/packages/core/src/task/LambdaTask.ts @@ -43,7 +43,7 @@ export class LambdaTask extends SingleTask { constructor(config: TaskConfig & { input?: LambdaTaskInput } = {}) { super(config); } - runReactive() { + async runReactive() { if (!this.runInputData.fn) { throw new Error("No runner provided"); } diff --git a/packages/core/src/task/base/ArrayTask.ts b/packages/core/src/task/base/ArrayTask.ts index 2579951..23c226b 100644 --- a/packages/core/src/task/base/ArrayTask.ts +++ b/packages/core/src/task/base/ArrayTask.ts @@ -174,8 +174,8 @@ export function arrayTaskFactory< return this; } - runReactive(): PluralOutputType { - const runDataOut = super.runReactive(); + async runReactive(): Promise { + const runDataOut = await super.runReactive(); this.runOutputData = collectPropertyValues( runDataOut.outputs ) as PluralOutputType; diff --git a/packages/core/src/task/base/Task.ts b/packages/core/src/task/base/Task.ts index cd49021..de56009 100644 --- a/packages/core/src/task/base/Task.ts +++ b/packages/core/src/task/base/Task.ts @@ -272,12 +272,12 @@ export abstract class TaskBase { throw new Error("Invalid input data"); } this.emit("start"); - const result = this.runReactive(); + const result = await this.runReactive(); this.emit("complete"); this.runOutputData = result; return result; } - runReactive(): TaskOutput { + async runReactive(): Promise { return this.runOutputData; } @@ -334,9 +334,9 @@ export class CompoundTask extends TaskBase implements ITaskCompound { this.emit("complete"); return this.runOutputData; } - runReactive(): TaskOutput { + async runReactive(): Promise { const runner = new TaskGraphRunner(this.subGraph); - this.runOutputData.outputs = runner.runGraphSyncOnly(); + this.runOutputData.outputs = await runner.runGraphReactive(); return this.runOutputData; } diff --git a/packages/core/src/task/base/TaskGraphRunner.ts b/packages/core/src/task/base/TaskGraphRunner.ts index e7f6586..81d59c6 100644 --- a/packages/core/src/task/base/TaskGraphRunner.ts +++ b/packages/core/src/task/base/TaskGraphRunner.ts @@ -96,7 +96,7 @@ export class TaskGraphRunner { task.emit("start"); task.emit("progress", 100, Object.values(results)[0]); task.runOutputData = results; - task.runReactive(); + await task.runReactive(); task.emit("complete"); } } @@ -132,23 +132,26 @@ export class TaskGraphRunner { return results; } - private runTasksSync() { + private async runTasksReactive() { let results: TaskOutput[] = []; for (const [_layerNumber, nodes] of this.layers.entries()) { - results = nodes.map((node) => { - this.copyInputFromEdgesToNode(node); - const results = node.runReactive(); - this.pushOutputFromNodeToEdges(node, results); - return results; - }); + const settledResults = await Promise.allSettled( + nodes.map(async (node) => { + this.copyInputFromEdgesToNode(node); + const results = await node.runReactive(); + this.pushOutputFromNodeToEdges(node, results); + return results; + }) + ); + results = settledResults.map((r) => (r.status === "fulfilled" ? r.value : {})); } return results; } - public runGraphSyncOnly() { + public async runGraphReactive() { this.dag.getNodes().forEach((node) => node.resetInputData()); const sortedNodes = this.dag.topologicallySortedNodes(); this.assignLayers(sortedNodes); - return this.runTasksSync(); + return await this.runTasksReactive(); } } diff --git a/packages/core/src/task/test/Task.test.ts b/packages/core/src/task/test/Task.test.ts index 02a9418..a493885 100644 --- a/packages/core/src/task/test/Task.test.ts +++ b/packages/core/src/task/test/Task.test.ts @@ -26,7 +26,7 @@ class TestTask extends SingleTask { ] as const; static readonly outputs = [ { - id: "syncOnly", + id: "reactiveOnly", name: "Output", valueType: "boolean", }, @@ -41,11 +41,11 @@ class TestTask extends SingleTask { valueType: "text", }, ] as const; - runReactive(): TestTaskOutput { - return { all: false, key: this.runInputData.key, syncOnly: true }; + async runReactive(): Promise { + return { all: false, key: this.runInputData.key, reactiveOnly: true }; } async run(): Promise { - return { all: true, key: this.runInputData.key, syncOnly: false }; + return { all: true, key: this.runInputData.key, reactiveOnly: false }; } } @@ -62,7 +62,7 @@ class TestCompoundTask extends CompoundTask { ] as const; static readonly outputs = [ { - id: "syncOnly", + id: "reactiveOnly", name: "Output", valueType: "boolean", }, @@ -78,12 +78,12 @@ class TestCompoundTask extends CompoundTask { }, ] as const; static readonly type = "TestCompoundTask"; - runReactive(): TestTaskOutput { - this.runOutputData = { key: this.runInputData.key, all: false, syncOnly: true }; + async runReactive(): Promise { + this.runOutputData = { key: this.runInputData.key, all: false, reactiveOnly: true }; return this.runOutputData; } async run(): Promise { - this.runOutputData = { key: this.runInputData.key, all: true, syncOnly: false }; + this.runOutputData = { key: this.runInputData.key, all: true, reactiveOnly: false }; return this.runOutputData; } } @@ -95,14 +95,14 @@ describe("Task", () => { const input = { key: "value" }; node.addInputData(input); const output = await node.run(); - expect(output).toEqual({ ...input, syncOnly: false, all: true }); + expect(output).toEqual({ ...input, reactiveOnly: false, all: true }); expect(node.runInputData).toEqual(input); }); - it("should run the task synchronously", () => { + it("should run the task reactively", async () => { const node = new TestTask(); - const output = node.runReactive(); - expect(output).toEqual({ key: "", syncOnly: true, all: false }); + const output = await node.runReactive(); + expect(output).toEqual({ key: "", reactiveOnly: true, all: false }); }); }); @@ -123,14 +123,14 @@ describe("Task", () => { const input = { key: "value" }; node.addInputData(input); const output = await node.run(); - expect(output).toEqual({ key: "value", all: true, syncOnly: false }); + expect(output).toEqual({ key: "value", all: true, reactiveOnly: false }); expect(node.runInputData).toEqual(input); }); - it("should run the task synchronously", () => { + it("should run the task synchronously", async () => { const node = new TestCompoundTask({ input: { key: "value2" } }); - const output = node.runReactive(); - expect(output).toEqual({ key: "value2", syncOnly: true, all: false }); + const output = await node.runReactive(); + expect(output).toEqual({ key: "value2", reactiveOnly: true, all: false }); }); }); }); diff --git a/packages/core/src/task/test/TaskGraph.test.ts b/packages/core/src/task/test/TaskGraph.test.ts index 02fd1b8..42462ce 100644 --- a/packages/core/src/task/test/TaskGraph.test.ts +++ b/packages/core/src/task/test/TaskGraph.test.ts @@ -11,7 +11,7 @@ import { TaskGraph, DataFlow, serialGraph } from "../base/TaskGraph"; class TestTask extends SingleTask { static readonly type = "TestTask"; - runReactive(): TaskOutput { + async runReactive(): Promise { return {}; } } diff --git a/packages/core/src/task/test/TaskGraphRunner.test.ts b/packages/core/src/task/test/TaskGraphRunner.test.ts index a5a43c4..b4f0495 100644 --- a/packages/core/src/task/test/TaskGraphRunner.test.ts +++ b/packages/core/src/task/test/TaskGraphRunner.test.ts @@ -13,7 +13,7 @@ import { CreateMappedType } from "../base/TaskIOTypes"; class TestTask extends SingleTask { static readonly type = "TestTask"; - runReactive(): TaskOutput { + async runReactive(): Promise { return {}; } } @@ -40,7 +40,7 @@ class TestSquareTask extends SingleTask { valueType: "number", }, ] as const; - runReactive(): TestSquareTaskOutput { + async runReactive(): Promise { return { output: this.runInputData.input * this.runInputData.input }; } } @@ -66,7 +66,7 @@ class TestDoubleTask extends SingleTask { valueType: "number", }, ] as const; - runReactive(): TestDoubleTaskOutput { + async runReactive(): Promise { return { output: this.runInputData.input * 2 }; } } @@ -98,7 +98,7 @@ class TestAddTask extends SingleTask { valueType: "number", }, ] as const; - runReactive(): TaskOutput { + async runReactive(): Promise { const input = this.runInputData; return { output: input.a + input.b }; } @@ -146,11 +146,11 @@ describe("TaskGraphRunner", () => { }); }); - describe("runGraphSyncOnly", () => { - it("should run nodes in each layer synchronously", () => { + describe("runGraphReactive", () => { + it("should run nodes in each layer synchronously", async () => { const runReactiveSpy = spyOn(nodes[0], "runReactive"); - runner.runGraphSyncOnly(); + await runner.runGraphReactive(); expect(runReactiveSpy).toHaveBeenCalledTimes(1); }); diff --git a/packages/core/src/task/test/TaskSubGraphRunner.test.ts b/packages/core/src/task/test/TaskSubGraphRunner.test.ts index b17c4bc..67654d9 100644 --- a/packages/core/src/task/test/TaskSubGraphRunner.test.ts +++ b/packages/core/src/task/test/TaskSubGraphRunner.test.ts @@ -37,7 +37,7 @@ class TestSquareTask extends SingleTask { valueType: "number", }, ] as const; - runReactive(): TestSquareTaskOutput { + async runReactive(): Promise { return { output: this.runInputData.input * this.runInputData.input }; } } @@ -63,7 +63,7 @@ class TestDoubleTask extends SingleTask { valueType: "number", }, ] as const; - runReactive(): TestDoubleTaskOutput { + async runReactive(): Promise { return { output: this.runInputData.input * 2 }; } } @@ -90,7 +90,7 @@ class TestAddTask extends SingleTask { valueType: "number", }, ] as const; - runReactive(): TaskOutput { + async runReactive(): Promise { const inputs = Array.isArray(this.runInputData.input) ? this.runInputData.input : [this.runInputData.input ?? 0]; diff --git a/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts b/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts index f113a6e..70f8360 100644 --- a/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts +++ b/packages/storage/src/browser/inmemory/test/InMemoryTaskGraphRepository.test.ts @@ -11,7 +11,7 @@ import { InMemoryTaskGraphRepository } from "ellmers-storage/inmemory"; class TestTask extends SingleTask { static readonly type = "TestTask"; - runReactive(): TaskOutput { + async runReactive(): Promise { return {}; } } diff --git a/packages/storage/src/bun/sqlite/test/SqliteTaskGraphRepository.test.ts b/packages/storage/src/bun/sqlite/test/SqliteTaskGraphRepository.test.ts index 989e97c..fe4ee07 100644 --- a/packages/storage/src/bun/sqlite/test/SqliteTaskGraphRepository.test.ts +++ b/packages/storage/src/bun/sqlite/test/SqliteTaskGraphRepository.test.ts @@ -11,7 +11,7 @@ import { SingleTask, TaskOutput, DataFlow, TaskGraph, TaskRegistry } from "ellme class TestTask extends SingleTask { static readonly type = "TestTask"; - runReactive(): TaskOutput { + async runReactive(): Promise { return {}; } } diff --git a/packages/storage/src/node/filesystem/test/FileTaskGraphRepository.test.ts b/packages/storage/src/node/filesystem/test/FileTaskGraphRepository.test.ts index 6bdce3e..9566cf0 100644 --- a/packages/storage/src/node/filesystem/test/FileTaskGraphRepository.test.ts +++ b/packages/storage/src/node/filesystem/test/FileTaskGraphRepository.test.ts @@ -12,7 +12,7 @@ import { SingleTask, TaskOutput, TaskRegistry, DataFlow, TaskGraph } from "ellme class TestTask extends SingleTask { static readonly type = "TestTask"; - runReactive(): TaskOutput { + async runReactive(): Promise { return {}; } } diff --git a/packages/task/src/task/JavaScriptTask.ts b/packages/task/src/task/JavaScriptTask.ts index 2600004..8264a10 100644 --- a/packages/task/src/task/JavaScriptTask.ts +++ b/packages/task/src/task/JavaScriptTask.ts @@ -46,7 +46,7 @@ export class JavaScriptTask extends SingleTask { constructor(config: TaskConfig & { input?: JavaScriptTaskInput } = {}) { super(config); } - runReactive() { + async runReactive() { if (this.runInputData.code) { try { const myInterpreter = new Interpreter(this.runInputData.code); From 21fe6e3b48f865d715713a94485bd22fefdbb668 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Fri, 17 Jan 2025 18:04:55 -0800 Subject: [PATCH 12/12] feat Model Repository and async model access --- examples/cli/src/TaskCLI.ts | 30 +++++---- examples/cli/src/ellmers.ts | 5 ++ examples/web/src/App.tsx | 9 ++- examples/web/src/main.tsx | 5 -- .../provider/HuggingFaceLocal_TaskRun.ts | 14 ++--- .../test/HFTransformersBinding.test.ts | 32 ++++++++-- .../provider/MediaPipeLocalTaskRun.ts | 4 +- packages/ai/src/model/ModelRegistry.ts | 20 +++--- packages/ai/src/model/ModelRepository.ts | 20 ++++-- packages/ai/src/task/DownloadModelTask.ts | 4 +- packages/ai/src/task/base/JobQueueLlmTask.ts | 2 +- .../inmemory/InMemoryModelRepository.ts | 2 +- .../inmemory/base/InMemoryKVRepository.ts | 4 +- .../storage/src/browser/inmemory/index.ts | 1 + .../test/InMemoryModelRepository.test.ts | 56 +++++++++++++++++ packages/storage/src/bun/sqlite/index.ts | 1 + .../sqlite/test/SqliteModelRepository.test.ts | 62 +++++++++++++++++++ .../test/src/sample/MediaPipeModelSamples.ts | 16 ++--- packages/test/src/sample/ONNXModelSamples.ts | 46 +++++++------- 19 files changed, 244 insertions(+), 89 deletions(-) create mode 100644 packages/storage/src/bun/sqlite/test/SqliteModelRepository.test.ts diff --git a/examples/cli/src/TaskCLI.ts b/examples/cli/src/TaskCLI.ts index cce0aac..0a2b692 100644 --- a/examples/cli/src/TaskCLI.ts +++ b/examples/cli/src/TaskCLI.ts @@ -10,11 +10,8 @@ import { runTask } from "./TaskStreamToListr2"; import "@huggingface/transformers"; import { TaskGraph, JsonTask, TaskGraphBuilder, JsonTaskItem } from "ellmers-core"; import { DownloadModelTask, getGlobalModelRepository } from "ellmers-ai"; -import { registerHuggingfaceLocalModels } from "ellmers-test"; import "ellmers-task"; -registerHuggingfaceLocalModels(); - export function AddBaseCommands(program: Command) { program .command("download") @@ -23,7 +20,7 @@ export function AddBaseCommands(program: Command) { .action(async (options) => { const graph = new TaskGraph(); if (options.model) { - const model = getGlobalModelRepository().findByName(options.model); + const model = await getGlobalModelRepository().findByName(options.model); if (model) { graph.addTask(new DownloadModelTask({ input: { model: model.name } })); } else { @@ -40,10 +37,11 @@ export function AddBaseCommands(program: Command) { .option("--model ", "model to use") .action(async (text: string, options) => { const model = options.model - ? getGlobalModelRepository().findByName(options.model)?.name - : getGlobalModelRepository() - .findModelsByTask("TextEmbeddingTask") - .map((m) => m.name); + ? (await getGlobalModelRepository().findByName(options.model))?.name + : (await getGlobalModelRepository().findModelsByTask("TextEmbeddingTask"))?.map( + (m) => m.name + ); + if (!model) { program.error(`Unknown model ${options.model}`); } else { @@ -60,10 +58,10 @@ export function AddBaseCommands(program: Command) { .option("--model ", "model to use") .action(async (text, options) => { const model = options.model - ? getGlobalModelRepository().findByName(options.model)?.name - : getGlobalModelRepository() - .findModelsByTask("TextSummaryTask") - .map((m) => m.name); + ? (await getGlobalModelRepository().findByName(options.model))?.name + : (await getGlobalModelRepository().findModelsByTask("TextSummaryTask"))?.map( + (m) => m.name + ); if (!model) { program.error(`Unknown model ${options.model}`); } else { @@ -81,10 +79,10 @@ export function AddBaseCommands(program: Command) { .option("--model ", "model to use") .action(async (text, options) => { const model = options.model - ? getGlobalModelRepository().findByName(options.model)?.name - : getGlobalModelRepository() - .findModelsByTask("TextRewriterTask") - .map((m) => m.name); + ? (await getGlobalModelRepository().findByName(options.model))?.name + : (await getGlobalModelRepository().findModelsByTask("TextRewriterTask"))?.map( + (m) => m.name + ); if (!model) { program.error(`Unknown model ${options.model}`); } else { diff --git a/examples/cli/src/ellmers.ts b/examples/cli/src/ellmers.ts index b164249..98e0e7f 100755 --- a/examples/cli/src/ellmers.ts +++ b/examples/cli/src/ellmers.ts @@ -5,8 +5,10 @@ import { argv } from "process"; import { AddBaseCommands } from "./TaskCLI"; import { getProviderRegistry } from "ellmers-ai"; import { + registerHuggingfaceLocalModels, registerHuggingfaceLocalTasksInMemory, registerMediaPipeTfJsLocalInMemory, + registerMediaPipeTfJsLocalModels, } from "ellmers-test"; import "ellmers-test"; @@ -14,6 +16,9 @@ program.version("1.0.0").description("A CLI to run Ellmers."); AddBaseCommands(program); +await registerHuggingfaceLocalModels(); +await registerMediaPipeTfJsLocalModels(); + registerHuggingfaceLocalTasksInMemory(); registerMediaPipeTfJsLocalInMemory(); diff --git a/examples/web/src/App.tsx b/examples/web/src/App.tsx index a3928c5..3c65522 100644 --- a/examples/web/src/App.tsx +++ b/examples/web/src/App.tsx @@ -48,9 +48,6 @@ ProviderRegistry.registerQueue( new InMemoryJobQueue("local_mp", new ConcurrencyLimiter(1, 10), 10) ); -registerHuggingfaceLocalModels(); -registerMediaPipeTfJsLocalModels(); - ProviderRegistry.clearQueues(); ProviderRegistry.startQueues(); @@ -114,6 +111,12 @@ export const App = () => { // changes coming from builder in console useEffect(() => { + async function init() { + await registerHuggingfaceLocalModels(); + await registerMediaPipeTfJsLocalModels(); + } + init(); + function listen() { setJsonData(JSON.stringify(builder.toDependencyJSON(), null, 2)); setGraph(builder.graph); diff --git a/examples/web/src/main.tsx b/examples/web/src/main.tsx index 80d7c70..0e3a945 100644 --- a/examples/web/src/main.tsx +++ b/examples/web/src/main.tsx @@ -87,11 +87,6 @@ console.log( ); console.log(window["builder"]); -console.log( - "Models Available: ", - getGlobalModelRepository().models.map((m) => m.name) -); - console.log( "Tasks Available: ", Array.from(TaskRegistry.all.entries()).map(([name]) => name) diff --git a/packages/ai-provider/src/hf-transformers/provider/HuggingFaceLocal_TaskRun.ts b/packages/ai-provider/src/hf-transformers/provider/HuggingFaceLocal_TaskRun.ts index f4913e9..c978935 100644 --- a/packages/ai-provider/src/hf-transformers/provider/HuggingFaceLocal_TaskRun.ts +++ b/packages/ai-provider/src/hf-transformers/provider/HuggingFaceLocal_TaskRun.ts @@ -140,7 +140,7 @@ export async function HuggingFaceLocal_DownloadRun( task: DownloadModelTask, runInputData: DownloadModelTaskInput ): Promise> { - const model = getGlobalModelRepository().findByName(runInputData.model)!; + const model = (await getGlobalModelRepository().findByName(runInputData.model))!; await getPipeline(task, model); return { model: model.name, dimensions: model.nativeDimensions || 0, normalize: model.normalize }; } @@ -154,7 +154,7 @@ export async function HuggingFaceLocal_EmbeddingRun( task: TextEmbeddingTask, runInputData: TextEmbeddingTaskInput ): Promise { - const model = getGlobalModelRepository().findByName(runInputData.model)!; + const model = (await getGlobalModelRepository().findByName(runInputData.model))!; const generateEmbedding: FeatureExtractionPipeline = await getPipeline(task, model); const hfVector = await generateEmbedding(runInputData.text, { @@ -183,7 +183,7 @@ export async function HuggingFaceLocal_TextGenerationRun( task: TextGenerationTask, runInputData: TextGenerationTaskInput ): Promise { - const model = getGlobalModelRepository().findByName(runInputData.model)!; + const model = (await getGlobalModelRepository().findByName(runInputData.model))!; const generateText: TextGenerationPipeline = await getPipeline(task, model); @@ -219,7 +219,7 @@ export async function HuggingFaceLocal_TextTranslationRun( task: TextTranslationTask, runInputData: TextTranslationTaskInput ): Promise> { - const model = getGlobalModelRepository().findByName(runInputData.model)!; + const model = (await getGlobalModelRepository().findByName(runInputData.model))!; const translate: TranslationPipeline = await getPipeline(task, model); @@ -251,7 +251,7 @@ export async function HuggingFaceLocal_TextRewriterRun( task: TextRewriterTask, runInputData: TextRewriterTaskInput ): Promise { - const model = getGlobalModelRepository().findByName(runInputData.model)!; + const model = (await getGlobalModelRepository().findByName(runInputData.model))!; const generateText: TextGenerationPipeline = await getPipeline(task, model); const streamer = new TextStreamer(generateText.tokenizer, { @@ -290,7 +290,7 @@ export async function HuggingFaceLocal_TextSummaryRun( task: TextSummaryTask, runInputData: TextSummaryTaskInput ): Promise { - const model = getGlobalModelRepository().findByName(runInputData.model)!; + const model = (await getGlobalModelRepository().findByName(runInputData.model))!; const generateSummary: SummarizationPipeline = await getPipeline(task, model); const streamer = new TextStreamer(generateSummary.tokenizer, { @@ -319,7 +319,7 @@ export async function HuggingFaceLocal_TextQuestionAnswerRun( task: TextQuestionAnswerTask, runInputData: TextQuestionAnswerTaskInput ): Promise { - const model = getGlobalModelRepository().findByName(runInputData.model)!; + const model = (await getGlobalModelRepository().findByName(runInputData.model))!; const generateAnswer: QuestionAnsweringPipeline = await getPipeline(task, model); const streamer = new TextStreamer(generateAnswer.tokenizer, { diff --git a/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts b/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts index 0251083..04a1410 100644 --- a/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts +++ b/packages/ai-provider/src/hf-transformers/test/HFTransformersBinding.test.ts @@ -7,8 +7,12 @@ import { describe, expect, it } from "bun:test"; import { ConcurrencyLimiter, TaskGraphBuilder, TaskInput, TaskOutput } from "ellmers-core"; -import { getProviderRegistry, getGlobalModelRepository } from "ellmers-ai"; -import { InMemoryJobQueue } from "ellmers-storage/inmemory"; +import { + getProviderRegistry, + getGlobalModelRepository, + setGlobalModelRepository, +} from "ellmers-ai"; +import { InMemoryJobQueue, InMemoryModelRepository } from "ellmers-storage/inmemory"; import { getDatabase, SqliteJobQueue } from "ellmers-storage/bun/sqlite"; import { registerHuggingfaceLocalTasks } from "../bindings/registerTasks"; import { sleep } from "bun"; @@ -19,7 +23,8 @@ const HFQUEUE = "local_hf"; describe("HFTransformersBinding", () => { describe("InMemoryJobQueue", () => { it("Should have an item queued", async () => { - getGlobalModelRepository().addModel({ + setGlobalModelRepository(new InMemoryModelRepository()); + await getGlobalModelRepository().addModel({ name: "ONNX Xenova/LaMini-Flan-T5-783M q8", url: "Xenova/LaMini-Flan-T5-783M", availableOnBrowser: true, @@ -27,11 +32,11 @@ describe("HFTransformersBinding", () => { provider: LOCAL_ONNX_TRANSFORMERJS, pipeline: "text2text-generation", }); - getGlobalModelRepository().connectTaskToModel( + await getGlobalModelRepository().connectTaskToModel( "TextGenerationTask", "ONNX Xenova/LaMini-Flan-T5-783M q8" ); - getGlobalModelRepository().connectTaskToModel( + await getGlobalModelRepository().connectTaskToModel( "TextRewritingTask", "ONNX Xenova/LaMini-Flan-T5-783M q8" ); @@ -62,6 +67,23 @@ describe("HFTransformersBinding", () => { describe("SqliteJobQueue", () => { it("Should have an item queued", async () => { registerHuggingfaceLocalTasks(); + setGlobalModelRepository(new InMemoryModelRepository()); + await getGlobalModelRepository().addModel({ + name: "ONNX Xenova/LaMini-Flan-T5-783M q8", + url: "Xenova/LaMini-Flan-T5-783M", + availableOnBrowser: true, + availableOnServer: true, + provider: LOCAL_ONNX_TRANSFORMERJS, + pipeline: "text2text-generation", + }); + await getGlobalModelRepository().connectTaskToModel( + "TextGenerationTask", + "ONNX Xenova/LaMini-Flan-T5-783M q8" + ); + await getGlobalModelRepository().connectTaskToModel( + "TextRewritingTask", + "ONNX Xenova/LaMini-Flan-T5-783M q8" + ); const providerRegistry = getProviderRegistry(); const jobQueue = new SqliteJobQueue( getDatabase(":memory:"), diff --git a/packages/ai-provider/src/tf-mediapipe/provider/MediaPipeLocalTaskRun.ts b/packages/ai-provider/src/tf-mediapipe/provider/MediaPipeLocalTaskRun.ts index 2a767fa..b32a307 100644 --- a/packages/ai-provider/src/tf-mediapipe/provider/MediaPipeLocalTaskRun.ts +++ b/packages/ai-provider/src/tf-mediapipe/provider/MediaPipeLocalTaskRun.ts @@ -25,7 +25,7 @@ export async function MediaPipeTfJsLocal_Download( const textFiles = await FilesetResolver.forTextTasks( "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-text@latest/wasm" ); - const model = getGlobalModelRepository().findByName(runInputData.model); + const model = (await getGlobalModelRepository().findByName(runInputData.model))!; if (!model) { throw `MediaPipeTfJsLocal_Download: Model ${runInputData.model} not found`; } @@ -50,7 +50,7 @@ export async function MediaPipeTfJsLocal_Embedding( const textFiles = await FilesetResolver.forTextTasks( "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-text@latest/wasm" ); - const model = getGlobalModelRepository().findByName(runInputData.model); + const model = (await getGlobalModelRepository().findByName(runInputData.model))!; if (!model) { throw `MediaPipeTfJsLocal_Embedding: Model ${runInputData.model} not found`; } diff --git a/packages/ai/src/model/ModelRegistry.ts b/packages/ai/src/model/ModelRegistry.ts index 354aaed..1200da2 100644 --- a/packages/ai/src/model/ModelRegistry.ts +++ b/packages/ai/src/model/ModelRegistry.ts @@ -6,43 +6,43 @@ // ******************************************************************************* import { Model } from "./Model"; -import { Task2ModelPrimaryKey } from "./ModelRepository"; +import { ModelRepository, Task2ModelPrimaryKey } from "./ModelRepository"; // temporary model registry that is synchronous until we have a proper model repository -class ModelRegistry { +class FallbackModelRegistry { models: Model[] = []; task2models: Task2ModelPrimaryKey[] = []; - addModel(model: Model) { + public async addModel(model: Model) { if (this.models.some((m) => m.name === model.name)) { this.models = this.models.filter((m) => m.name !== model.name); } this.models.push(model); } - findModelsByTask(task: string) { + public async findModelsByTask(task: string) { return this.task2models .filter((t2m) => t2m.task === task) .map((t2m) => this.models.find((m) => m.name === t2m.model)) .filter((m) => m !== undefined); } - findTasksByModel(name: string) { + public async findTasksByModel(name: string) { return this.task2models.filter((t2m) => t2m.model === name).map((t2m) => t2m.task); } - findByName(name: string) { + public async findByName(name: string) { return this.models.find((m) => m.name === name); } - connectTaskToModel(task: string, model: string) { + public async connectTaskToModel(task: string, model: string) { this.task2models.push({ task, model }); } } -let modelRegistry: ModelRegistry; +let modelRegistry: FallbackModelRegistry | ModelRepository; export function getGlobalModelRepository() { - if (!modelRegistry) modelRegistry = new ModelRegistry(); + if (!modelRegistry) modelRegistry = new FallbackModelRegistry(); return modelRegistry; } -export function setGlobalModelRepository(pr: ModelRegistry) { +export function setGlobalModelRepository(pr: FallbackModelRegistry | ModelRepository) { modelRegistry = pr; } diff --git a/packages/ai/src/model/ModelRepository.ts b/packages/ai/src/model/ModelRepository.ts index d61b913..531238f 100644 --- a/packages/ai/src/model/ModelRepository.ts +++ b/packages/ai/src/model/ModelRepository.ts @@ -27,7 +27,7 @@ export type ModelEvents = export type Task2ModelPrimaryKey = { /** The task identifier */ task: string; - /** The model identifier */ + /** The model name identifier */ model: string; }; @@ -120,7 +120,7 @@ export abstract class ModelRepository { * @param task - The task identifier to search for * @returns Promise resolving to an array of associated models, or undefined if none found */ - async findModelByTask(task: string) { + async findModelsByTask(task: string) { if (typeof task != "string") return undefined; const junctions = await this.task2ModelKvRepository.search({ task }); if (!junctions || junctions.length === 0) return undefined; @@ -132,13 +132,25 @@ export abstract class ModelRepository { return models; } + /** + * Finds all tasks associated with a specific model + * @param model - The model identifier to search for + * @returns Promise resolving to an array of associated tasks, or undefined if none found + */ + async findTasksByModel(model: string) { + if (typeof model != "string") return undefined; + const junctions = await this.task2ModelKvRepository.search({ model }); + if (!junctions || junctions.length === 0) return undefined; + return junctions.map((junction) => junction.task); + } + /** * Creates an association between a task and a model * @param task - The task identifier * @param model - The model to associate with the task */ - async connectModelToTask(task: string, model: Model) { - this.task2ModelKvRepository.put({ task, model: model.name }, { details: null }); + async connectTaskToModel(task: string, model: string) { + await this.task2ModelKvRepository.putKeyValue({ task, model }, { details: null }); this.emit("task_model_connected", task, model); } diff --git a/packages/ai/src/task/DownloadModelTask.ts b/packages/ai/src/task/DownloadModelTask.ts index 17ca2aa..5a716e2 100644 --- a/packages/ai/src/task/DownloadModelTask.ts +++ b/packages/ai/src/task/DownloadModelTask.ts @@ -80,9 +80,9 @@ export class DownloadModelTask extends JobQueueLlmTask { super(config); } async runReactive(): Promise { - const model = getGlobalModelRepository().findByName(this.runInputData.model); + const model = await getGlobalModelRepository().findByName(this.runInputData.model); if (model) { - const tasks = getGlobalModelRepository().findTasksByModel(model.name); + const tasks = (await getGlobalModelRepository().findTasksByModel(model.name)) || []; tasks.forEach((task) => { // this.runOutputData[String(task).toLowerCase()] = model.name; }); diff --git a/packages/ai/src/task/base/JobQueueLlmTask.ts b/packages/ai/src/task/base/JobQueueLlmTask.ts index f436414..f2e4070 100644 --- a/packages/ai/src/task/base/JobQueueLlmTask.ts +++ b/packages/ai/src/task/base/JobQueueLlmTask.ts @@ -35,7 +35,7 @@ export class JobQueueLlmTask extends JobQueueTask { const ProviderRegistry = getProviderRegistry(); const modelname = this.runInputData["model"]; if (!modelname) throw new Error("JobQueueTaskTask: No model name found"); - const model = getGlobalModelRepository().findByName(modelname); + const model = await getGlobalModelRepository().findByName(modelname); if (!model) { throw new Error(`JobQueueTaskTask: No model ${modelname} found ${modelname}`); diff --git a/packages/storage/src/browser/inmemory/InMemoryModelRepository.ts b/packages/storage/src/browser/inmemory/InMemoryModelRepository.ts index 373233c..a4fd0d0 100644 --- a/packages/storage/src/browser/inmemory/InMemoryModelRepository.ts +++ b/packages/storage/src/browser/inmemory/InMemoryModelRepository.ts @@ -44,6 +44,6 @@ export class InMemoryModelRepository extends ModelRepository { Task2ModelDetail, typeof Task2ModelPrimaryKeySchema, typeof Task2ModelDetailSchema - >(Task2ModelPrimaryKeySchema, Task2ModelDetailSchema); + >(Task2ModelPrimaryKeySchema, Task2ModelDetailSchema, ["model"]); } } diff --git a/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts b/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts index 84efc1a..5defaa0 100644 --- a/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts +++ b/packages/storage/src/browser/inmemory/base/InMemoryKVRepository.ts @@ -83,8 +83,8 @@ export class InMemoryKVRepository< } /** - * Searches for entries matching a partial key-value pair - * @param key - Partial combined key-value object to search for + * Searches for entries matching a partial key + * @param key - Partial key object to search for * @returns Array of matching combined objects * @throws Error if search criteria contains more than one key */ diff --git a/packages/storage/src/browser/inmemory/index.ts b/packages/storage/src/browser/inmemory/index.ts index fe1fefc..c892320 100644 --- a/packages/storage/src/browser/inmemory/index.ts +++ b/packages/storage/src/browser/inmemory/index.ts @@ -3,3 +3,4 @@ export * from "./InMemoryTaskOutputRepository"; export * from "./InMemoryTaskGraphRepository"; export * from "./InMemoryJobQueue"; export * from "./InMemoryRateLimiter"; +export * from "./InMemoryModelRepository"; diff --git a/packages/storage/src/browser/inmemory/test/InMemoryModelRepository.test.ts b/packages/storage/src/browser/inmemory/test/InMemoryModelRepository.test.ts index e69de29..496bc95 100644 --- a/packages/storage/src/browser/inmemory/test/InMemoryModelRepository.test.ts +++ b/packages/storage/src/browser/inmemory/test/InMemoryModelRepository.test.ts @@ -0,0 +1,56 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { describe, expect, it, beforeEach } from "bun:test"; +import { setGlobalModelRepository, getGlobalModelRepository } from "ellmers-ai"; +import { InMemoryModelRepository } from "../InMemoryModelRepository"; +import { LOCAL_ONNX_TRANSFORMERJS } from "ellmers-ai-provider/hf-transformers/server"; + +describe("InMemoryModelRepository", () => { + it("store and find model by task", async () => { + setGlobalModelRepository(new InMemoryModelRepository()); + await getGlobalModelRepository().addModel({ + name: "ONNX Xenova/LaMini-Flan-T5-783M q8", + url: "Xenova/LaMini-Flan-T5-783M", + availableOnBrowser: true, + availableOnServer: true, + provider: LOCAL_ONNX_TRANSFORMERJS, + pipeline: "text2text-generation", + }); + await getGlobalModelRepository().connectTaskToModel( + "TextGenerationTask", + "ONNX Xenova/LaMini-Flan-T5-783M q8" + ); + await getGlobalModelRepository().connectTaskToModel( + "TextRewritingTask", + "ONNX Xenova/LaMini-Flan-T5-783M q8" + ); + const tasks = await getGlobalModelRepository().findTasksByModel( + "ONNX Xenova/LaMini-Flan-T5-783M q8" + ); + expect(tasks).toBeDefined(); + expect(tasks?.length).toEqual(2); + const models = await getGlobalModelRepository().findModelsByTask("TextGenerationTask"); + expect(models).toBeDefined(); + expect(models?.length).toEqual(1); + }); + it("store and find model by name", async () => { + setGlobalModelRepository(new InMemoryModelRepository()); + await getGlobalModelRepository().addModel({ + name: "ONNX Xenova/LaMini-Flan-T5-783M q8", + url: "Xenova/LaMini-Flan-T5-783M", + availableOnBrowser: true, + availableOnServer: true, + provider: LOCAL_ONNX_TRANSFORMERJS, + pipeline: "text2text-generation", + }); + + const model = await getGlobalModelRepository().findByName("ONNX Xenova/LaMini-Flan-T5-783M q8"); + expect(model).toBeDefined(); + expect(model?.name).toEqual("ONNX Xenova/LaMini-Flan-T5-783M q8"); + }); +}); diff --git a/packages/storage/src/bun/sqlite/index.ts b/packages/storage/src/bun/sqlite/index.ts index 3b1728b..6550cbb 100644 --- a/packages/storage/src/bun/sqlite/index.ts +++ b/packages/storage/src/bun/sqlite/index.ts @@ -2,3 +2,4 @@ export { getDatabase } from "../../util/db_sqlite"; export * from "./SqliteJobQueue"; export * from "./SqliteTaskGraphRepository"; export * from "./SqliteTaskOutputRepository"; +export * from "./SqliteModelRepository"; diff --git a/packages/storage/src/bun/sqlite/test/SqliteModelRepository.test.ts b/packages/storage/src/bun/sqlite/test/SqliteModelRepository.test.ts new file mode 100644 index 0000000..74edc6c --- /dev/null +++ b/packages/storage/src/bun/sqlite/test/SqliteModelRepository.test.ts @@ -0,0 +1,62 @@ +// ******************************************************************************* +// * ELLMERS: Embedding Large Language Model Experiential Retrieval Service * +// * * +// * Copyright Steven Roussey * +// * Licensed under the Apache License, Version 2.0 (the "License"); * +// ******************************************************************************* + +import { describe, expect, it, beforeEach } from "bun:test"; +import { setGlobalModelRepository, getGlobalModelRepository } from "ellmers-ai"; +import { SqliteModelRepository } from "../SqliteModelRepository"; +import { LOCAL_ONNX_TRANSFORMERJS } from "ellmers-ai-provider/hf-transformers/server"; + +describe("SqliteModelRepository", () => { + it("store and find model by task", async () => { + setGlobalModelRepository(new SqliteModelRepository(":memory:")); + await getGlobalModelRepository().addModel({ + name: "ONNX Xenova/LaMini-Flan-T5-783M q8", + url: "Xenova/LaMini-Flan-T5-783M", + availableOnBrowser: true, + availableOnServer: true, + provider: LOCAL_ONNX_TRANSFORMERJS, + pipeline: "text2text-generation", + }); + await getGlobalModelRepository().connectTaskToModel( + "TextGenerationTask", + "ONNX Xenova/LaMini-Flan-T5-783M q8" + ); + await getGlobalModelRepository().connectTaskToModel( + "TextRewritingTask", + "ONNX Xenova/LaMini-Flan-T5-783M q8" + ); + const tasks = await getGlobalModelRepository().findTasksByModel( + "ONNX Xenova/LaMini-Flan-T5-783M q8" + ); + expect(tasks).toBeDefined(); + expect(tasks?.length).toEqual(2); + const models = await getGlobalModelRepository().findModelsByTask("TextGenerationTask"); + expect(models).toBeDefined(); + expect(models?.length).toEqual(1); + }); + it("store and find model by name", async () => { + setGlobalModelRepository(new SqliteModelRepository(":memory:")); + await getGlobalModelRepository().addModel({ + name: "ONNX Xenova/LaMini-Flan-T5-783M q8", + url: "Xenova/LaMini-Flan-T5-783M", + availableOnBrowser: true, + availableOnServer: true, + provider: LOCAL_ONNX_TRANSFORMERJS, + pipeline: "text2text-generation", + }); + + const model = await getGlobalModelRepository().findByName("ONNX Xenova/LaMini-Flan-T5-783M q8"); + expect(model).toBeDefined(); + expect(model?.name).toEqual("ONNX Xenova/LaMini-Flan-T5-783M q8"); + const tasks = await getGlobalModelRepository().findTasksByModel( + "ONNX Xenova/LaMini-Flan-T5-783M q8" + ); + expect(tasks).toBeUndefined(); + const model2 = await getGlobalModelRepository().findByName("ONNX Xenova/no-exist"); + expect(model2).toBeUndefined(); + }); +}); diff --git a/packages/test/src/sample/MediaPipeModelSamples.ts b/packages/test/src/sample/MediaPipeModelSamples.ts index a44ed5f..f15ae35 100644 --- a/packages/test/src/sample/MediaPipeModelSamples.ts +++ b/packages/test/src/sample/MediaPipeModelSamples.ts @@ -1,7 +1,7 @@ import { MEDIA_PIPE_TFJS_MODEL } from "ellmers-ai-provider/tf-mediapipe/browser"; import { getGlobalModelRepository, Model } from "ellmers-ai"; -function addMediaPipeModel(info: Partial, tasks: string[]) { +async function addMediaPipeModel(info: Partial, tasks: string[]) { const name = "MEDIAPIPE " + info.name; const model = Object.assign( @@ -20,14 +20,14 @@ function addMediaPipeModel(info: Partial, tasks: string[]) { { name } ) as Model; - getGlobalModelRepository().addModel(model); - tasks.forEach((task) => { - getGlobalModelRepository().connectTaskToModel(task, name); - }); + await getGlobalModelRepository().addModel(model); + await Promise.allSettled( + tasks.map((task) => getGlobalModelRepository().connectTaskToModel(task, name)) + ); } -export function registerMediaPipeTfJsLocalModels() { - addMediaPipeModel( +export async function registerMediaPipeTfJsLocalModels(): Promise { + await addMediaPipeModel( { name: "Universal Sentence Encoder", pipeline: "text_embedder", @@ -37,7 +37,7 @@ export function registerMediaPipeTfJsLocalModels() { ["TextEmbeddingTask"] ); - addMediaPipeModel( + await addMediaPipeModel( { name: "Text Encoder", pipeline: "text_embedder", diff --git a/packages/test/src/sample/ONNXModelSamples.ts b/packages/test/src/sample/ONNXModelSamples.ts index b88226e..cc127d0 100644 --- a/packages/test/src/sample/ONNXModelSamples.ts +++ b/packages/test/src/sample/ONNXModelSamples.ts @@ -4,7 +4,7 @@ import { } from "ellmers-ai-provider/hf-transformers/browser"; import { getGlobalModelRepository, Model } from "ellmers-ai"; -function addONNXModel(info: Partial, tasks: string[]) { +async function addONNXModel(info: Partial, tasks: string[]) { const name = info.name ? info.name : "ONNX " + info.url + " " + (info.quantization ?? QUANTIZATION_DATA_TYPES.q8); @@ -25,14 +25,14 @@ function addONNXModel(info: Partial, tasks: string[]) { { name } ) as Model; - getGlobalModelRepository().addModel(model); - tasks.forEach((task) => { - getGlobalModelRepository().connectTaskToModel(task, name); - }); + await getGlobalModelRepository().addModel(model); + await Promise.allSettled( + tasks.map((task) => getGlobalModelRepository().connectTaskToModel(task, name)) + ); } -export function registerHuggingfaceLocalModels() { - addONNXModel( +export async function registerHuggingfaceLocalModels(): Promise { + await addONNXModel( { pipeline: "feature-extraction", nativeDimensions: 384, @@ -41,7 +41,7 @@ export function registerHuggingfaceLocalModels() { ["TextEmbeddingTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "feature-extraction", nativeDimensions: 768, @@ -50,7 +50,7 @@ export function registerHuggingfaceLocalModels() { ["TextEmbeddingTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "feature-extraction", nativeDimensions: 384, @@ -59,7 +59,7 @@ export function registerHuggingfaceLocalModels() { ["TextEmbeddingTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "feature-extraction", nativeDimensions: 1024, @@ -68,7 +68,7 @@ export function registerHuggingfaceLocalModels() { ["TextEmbeddingTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "feature-extraction", nativeDimensions: 384, @@ -76,7 +76,7 @@ export function registerHuggingfaceLocalModels() { }, ["TextEmbeddingTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "question-answering", url: "Xenova/distilbert-base-uncased-distilled-squad", @@ -84,7 +84,7 @@ export function registerHuggingfaceLocalModels() { ["TextQuestionAnsweringTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "zero-shot-classification", url: "Xenova/distilbert-base-uncased-mnli", @@ -92,7 +92,7 @@ export function registerHuggingfaceLocalModels() { ["TextClassificationTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "fill-mask", url: "answerdotai/ModernBERT-base", @@ -100,7 +100,7 @@ export function registerHuggingfaceLocalModels() { ["TextClassificationTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "feature-extraction", nativeDimensions: 768, @@ -109,7 +109,7 @@ export function registerHuggingfaceLocalModels() { ["TextEmbeddingTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "text-generation", url: "Xenova/gpt2", @@ -117,7 +117,7 @@ export function registerHuggingfaceLocalModels() { ["TextGenerationTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "text-generation", url: "Xenova/distilgpt2", @@ -125,7 +125,7 @@ export function registerHuggingfaceLocalModels() { ["TextGenerationTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "text2text-generation", url: "Xenova/flan-t5-small", @@ -133,7 +133,7 @@ export function registerHuggingfaceLocalModels() { ["TextGenerationTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "text2text-generation", url: "Xenova/LaMini-Flan-T5-783M", @@ -141,7 +141,7 @@ export function registerHuggingfaceLocalModels() { ["TextGenerationTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "summarization", url: "Falconsai/text_summarization", @@ -149,7 +149,7 @@ export function registerHuggingfaceLocalModels() { ["TextSummaryTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "translation", url: "Xenova/nllb-200-distilled-600M", @@ -158,7 +158,7 @@ export function registerHuggingfaceLocalModels() { ["TextTranslationTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "translation", url: "Xenova/m2m100_418M", @@ -167,7 +167,7 @@ export function registerHuggingfaceLocalModels() { ["TextTranslationTask"] ); - addONNXModel( + await addONNXModel( { pipeline: "translation", url: "Xenova/mbart-large-50-many-to-many-mmt",