mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 09:29:38 +02:00
saving
This commit is contained in:
parent
e8c7a4f6e0
commit
ffd97375a8
160 changed files with 6704 additions and 1895 deletions
|
|
@ -1,15 +1,14 @@
|
||||||
# TrustGraph TypeScript — multi-stage build for all Node.js services.
|
# TrustGraph TypeScript — multi-stage build for all Bun services.
|
||||||
# A single image is built once; each service overrides CMD to pick its entrypoint.
|
# A single image is built once; each service overrides CMD to pick its entrypoint.
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
FROM node:22-slim AS builder
|
FROM oven/bun:1.3.13-slim AS builder
|
||||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy workspace config first for layer caching
|
# Copy workspace config first for layer caching
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsconfig.base.json tsconfig.json ./
|
COPY package.json bun.lock pnpm-workspace.yaml turbo.json tsconfig.base.json tsconfig.json ./
|
||||||
COPY packages/base/package.json packages/base/tsconfig.json packages/base/
|
COPY packages/base/package.json packages/base/tsconfig.json packages/base/
|
||||||
COPY packages/client/package.json packages/client/tsconfig.json packages/client/
|
COPY packages/client/package.json packages/client/tsconfig.json packages/client/
|
||||||
COPY packages/flow/package.json packages/flow/tsconfig.json packages/flow/
|
COPY packages/flow/package.json packages/flow/tsconfig.json packages/flow/
|
||||||
|
|
@ -17,16 +16,16 @@ COPY packages/cli/package.json packages/cli/tsconfig.json packages/cli/
|
||||||
COPY packages/mcp/package.json packages/mcp/tsconfig.json packages/mcp/
|
COPY packages/mcp/package.json packages/mcp/tsconfig.json packages/mcp/
|
||||||
COPY packages/workbench/package.json packages/workbench/tsconfig.json packages/workbench/
|
COPY packages/workbench/package.json packages/workbench/tsconfig.json packages/workbench/
|
||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
# Copy source and build
|
# Copy source and build
|
||||||
COPY packages/ packages/
|
COPY packages/ packages/
|
||||||
RUN pnpm build --filter=@trustgraph/base --filter=@trustgraph/client --filter=@trustgraph/flow
|
RUN bunx --bun turbo build --filter=@trustgraph/base --filter=@trustgraph/client --filter=@trustgraph/flow
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Stage 2: Runtime
|
# Stage 2: Runtime
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
FROM node:22-slim AS runtime
|
FROM oven/bun:1.3.13-slim AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy built output and production deps
|
# Copy built output and production deps
|
||||||
|
|
@ -41,4 +40,4 @@ ENV NODE_ENV=production
|
||||||
ENV NATS_URL=nats://nats:4222
|
ENV NATS_URL=nats://nats:4222
|
||||||
|
|
||||||
EXPOSE 8088
|
EXPOSE 8088
|
||||||
CMD ["node", "entrypoints/gateway.mjs"]
|
CMD ["bun", "entrypoints/gateway.mjs"]
|
||||||
|
|
|
||||||
255
ts/bun.lock
255
ts/bun.lock
|
|
@ -4,23 +4,38 @@
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trustgraph-ts",
|
"name": "trustgraph-ts",
|
||||||
|
"dependencies": {
|
||||||
|
"effect": "4.0.0-beta.65",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/platform-bun": "4.0.0-beta.65",
|
||||||
|
"@effect/tsgo": "0.6.0",
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
|
"@types/bun": "^1.3.13",
|
||||||
|
"@types/node": "^25.7.0",
|
||||||
|
"@typescript/native-preview": "^7.0.0-dev.20260511.1",
|
||||||
|
"falkordb": "^5.0.0",
|
||||||
|
"nats": "^2.29.0",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"turbo": "^2.5.0",
|
"turbo": "^2.5.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
|
"vitest": "^4.1.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/base": {
|
"packages/base": {
|
||||||
"name": "@trustgraph/base",
|
"name": "@trustgraph/base",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"effect": "4.0.0-beta.65",
|
||||||
"nats": "^2.29.0",
|
"nats": "^2.29.0",
|
||||||
"prom-client": "^15.1.0",
|
"prom-client": "^15.1.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^3.1.0",
|
"vitest": "^4.1.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
|
|
@ -33,23 +48,29 @@
|
||||||
"@trustgraph/base": "workspace:*",
|
"@trustgraph/base": "workspace:*",
|
||||||
"@trustgraph/client": "workspace:*",
|
"@trustgraph/client": "workspace:*",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
|
"effect": "4.0.0-beta.65",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
"@types/ws": "^8.5.0",
|
"@types/ws": "^8.5.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^3.1.0",
|
"vitest": "^4.1.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/client": {
|
"packages/client": {
|
||||||
"name": "@trustgraph/client",
|
"name": "@trustgraph/client",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"effect": "4.0.0-beta.65",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/ws": "^8.5.0",
|
"@types/ws": "^8.5.0",
|
||||||
"happy-dom": "^20.0.0",
|
"happy-dom": "^20.0.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^3.1.0",
|
"vitest": "^4.1.6",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"ws": "^8.0.0",
|
"ws": "^8.0.0",
|
||||||
|
|
@ -63,16 +84,24 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.39.0",
|
"@anthropic-ai/sdk": "^0.39.0",
|
||||||
|
"@effect/platform-bun": "4.0.0-beta.65",
|
||||||
"@fastify/websocket": "^11.0.0",
|
"@fastify/websocket": "^11.0.0",
|
||||||
|
"@mistralai/mistralai": "^1.0.0",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||||
"@qdrant/js-client-rest": "^1.13.0",
|
"@qdrant/js-client-rest": "^1.13.0",
|
||||||
"@trustgraph/base": "workspace:*",
|
"@trustgraph/base": "workspace:*",
|
||||||
|
"effect": "4.0.0-beta.65",
|
||||||
"falkordb": "^5.0.0",
|
"falkordb": "^5.0.0",
|
||||||
"fastify": "^5.2.0",
|
"fastify": "^5.2.0",
|
||||||
|
"ollama": "^0.6.3",
|
||||||
"openai": "^4.85.0",
|
"openai": "^4.85.0",
|
||||||
|
"pdfjs-dist": "^5.6.205",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^3.1.0",
|
"vitest": "^4.1.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/mcp": {
|
"packages/mcp": {
|
||||||
|
|
@ -82,12 +111,14 @@
|
||||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
"@modelcontextprotocol/sdk": "^1.8.0",
|
||||||
"@trustgraph/base": "workspace:*",
|
"@trustgraph/base": "workspace:*",
|
||||||
"@trustgraph/client": "workspace:*",
|
"@trustgraph/client": "workspace:*",
|
||||||
|
"effect": "4.0.0-beta.65",
|
||||||
"zod": "^3.23.0",
|
"zod": "^3.23.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^3.1.0",
|
"vitest": "^4.1.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/workbench": {
|
"packages/workbench": {
|
||||||
|
|
@ -96,7 +127,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.75.0",
|
"@tanstack/react-query": "^5.75.0",
|
||||||
"@trustgraph/client": "workspace:*",
|
"@trustgraph/client": "workspace:*",
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"lucide-react": "^0.513.0",
|
"lucide-react": "^0.513.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
|
@ -108,6 +138,7 @@
|
||||||
"zustand": "^5.0.0",
|
"zustand": "^5.0.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
"@tailwindcss/vite": "^4.1.0",
|
"@tailwindcss/vite": "^4.1.0",
|
||||||
"@types/react": "^19.1.0",
|
"@types/react": "^19.1.0",
|
||||||
"@types/react-dom": "^19.1.0",
|
"@types/react-dom": "^19.1.0",
|
||||||
|
|
@ -159,6 +190,28 @@
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "7.27.1", "@babel/helper-validator-identifier": "7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "7.27.1", "@babel/helper-validator-identifier": "7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
|
"@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.65", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.65" }, "peerDependencies": { "effect": "^4.0.0-beta.65" } }, "sha512-6BgEjVDibeOgGj0pvYRx+mBLSWVBPlvuqa6o4kuyrRIdgn92nbKrbglEiIRe4sn7yYmeKei4N/kd7fRzN3rEwA=="],
|
||||||
|
|
||||||
|
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.65", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.65" } }, "sha512-3rY8F3WLEax6Hj08GI/OvDIH+KqjfxH7RM2bAMfgR75NgRmwDtny1P49PtPkoRjH5dcdtThThtsvE4X9OTZkpQ=="],
|
||||||
|
|
||||||
|
"@effect/tsgo": ["@effect/tsgo@0.6.0", "", { "optionalDependencies": { "@effect/tsgo-darwin-arm64": "0.6.0", "@effect/tsgo-darwin-x64": "0.6.0", "@effect/tsgo-linux-arm": "0.6.0", "@effect/tsgo-linux-arm64": "0.6.0", "@effect/tsgo-linux-x64": "0.6.0", "@effect/tsgo-win32-arm64": "0.6.0", "@effect/tsgo-win32-x64": "0.6.0" }, "bin": { "effect-tsgo": "dist/effect-tsgo.js" } }, "sha512-suCUiGQ4Nkuw08kx3HJsS4PDtoc7h9erjTym8D8jdnlERt9RJ8bFfg5TnFwbriyyTZhjZCr7W0WctWyEMayotg=="],
|
||||||
|
|
||||||
|
"@effect/tsgo-darwin-arm64": ["@effect/tsgo-darwin-arm64@0.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mV9GI5wy6pAVssXV8awYCafr/AWhCoO6/xUJD2yv4MWEycP1/k9ZLR2mPvPMXqt51Zs9rkRGaCANkCKcRoxs3w=="],
|
||||||
|
|
||||||
|
"@effect/tsgo-darwin-x64": ["@effect/tsgo-darwin-x64@0.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-B/7V34BZqMqCYbP+TBz6ucTvc49gKhkoUnebELxK6imBymBS7fRkgsFd+trlst62bCblH774T2GCCx7gkyKmZw=="],
|
||||||
|
|
||||||
|
"@effect/tsgo-linux-arm": ["@effect/tsgo-linux-arm@0.6.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ovehHAdBbuxQi9eOon012JmcaVkxufWNSVJTpknlD+cC4jawiWDGIr51abkvH72GnTLgyfjjq0e/ibeJ5n9I0Q=="],
|
||||||
|
|
||||||
|
"@effect/tsgo-linux-arm64": ["@effect/tsgo-linux-arm64@0.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-xClS3A78/uM18ndxoGJDTxIe5AEbm5kuZ0ERJ/9wuQNRqITYW6ug93QMqsyDkp0VmFXxdfQbt81y4mpeSN1voQ=="],
|
||||||
|
|
||||||
|
"@effect/tsgo-linux-x64": ["@effect/tsgo-linux-x64@0.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-un+yA+AQShNSKxYJibhgY90c9bNPkjOZr0ecsmDB+S76STKQHOag/KW8G2EwpRg/eqWqn5GV04VEhP6Cq4QFMQ=="],
|
||||||
|
|
||||||
|
"@effect/tsgo-win32-arm64": ["@effect/tsgo-win32-arm64@0.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-5Ymu5FzNHA/YpDJkE67zD/KDPKm81e3BdlsGJ50ZOQI0YDnFlUWtENOjV3ScfW8g17pM3COnBJKJYYhMuek6wA=="],
|
||||||
|
|
||||||
|
"@effect/tsgo-win32-x64": ["@effect/tsgo-win32-x64@0.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-vD02OcS3zzRY7/vmGZiJaAttXymicpSY19FdSKHgvT9RvRlffbAFj90DSP6lFIrpZPMB2r18tmI2QrE0ZPzWWw=="],
|
||||||
|
|
||||||
|
"@effect/vitest": ["@effect/vitest@4.0.0-beta.65", "", { "peerDependencies": { "effect": "^4.0.0-beta.65", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-dJdZlQhB+AtMlSgrJ0QiprRhDnGVTcKmPe699+f5qkQjCKauogaAKekWV3xEM1envAMntwqeFUD4QHVnE+cFPg=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
|
||||||
|
|
@ -241,10 +294,52 @@
|
||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@mistralai/mistralai": ["@mistralai/mistralai@1.15.1", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-fb995eiz3r0KsBGtRjFV+/iLbX+UpfalxpF+YitT3R6ukrPD4PN+FGwwmYcRFhNAzVzDUtTVxQYnjQWEnwV5nw=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "1.19.12", "ajv": "8.18.0", "ajv-formats": "3.0.1", "content-type": "1.0.5", "cors": "2.8.6", "cross-spawn": "7.0.6", "eventsource": "3.0.7", "eventsource-parser": "3.0.6", "express": "5.2.1", "express-rate-limit": "8.3.2", "hono": "4.12.10", "jose": "6.2.2", "json-schema-typed": "8.0.2", "pkce-challenge": "5.0.1", "raw-body": "3.0.2", "zod-to-json-schema": "3.25.2" }, "peerDependencies": { "zod": "3.25.76" } }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "1.19.12", "ajv": "8.18.0", "ajv-formats": "3.0.1", "content-type": "1.0.5", "cors": "2.8.6", "cross-spawn": "7.0.6", "eventsource": "3.0.7", "eventsource-parser": "3.0.6", "express": "5.2.1", "express-rate-limit": "8.3.2", "hono": "4.12.10", "jose": "6.2.2", "json-schema-typed": "8.0.2", "pkce-challenge": "5.0.1", "raw-body": "3.0.2", "zod-to-json-schema": "3.25.2" }, "peerDependencies": { "zod": "3.25.76" } }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas": ["@napi-rs/canvas@0.1.100", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.100", "@napi-rs/canvas-darwin-arm64": "0.1.100", "@napi-rs/canvas-darwin-x64": "0.1.100", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", "@napi-rs/canvas-linux-arm64-musl": "0.1.100", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", "@napi-rs/canvas-linux-x64-gnu": "0.1.100", "@napi-rs/canvas-linux-x64-musl": "0.1.100", "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", "@napi-rs/canvas-win32-x64-msvc": "0.1.100" } }, "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.100", "", { "os": "android", "cpu": "arm64" }, "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.100", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.100", "", { "os": "darwin", "cpu": "x64" }, "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.100", "", { "os": "linux", "cpu": "arm" }, "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.100", "", { "os": "linux", "cpu": "arm64" }, "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.100", "", { "os": "linux", "cpu": "arm64" }, "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.100", "", { "os": "linux", "cpu": "none" }, "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.100", "", { "os": "linux", "cpu": "x64" }, "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.100", "", { "os": "linux", "cpu": "x64" }, "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@0.1.100", "", { "os": "win32", "cpu": "arm64" }, "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.100", "", { "os": "win32", "cpu": "x64" }, "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA=="],
|
||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||||
|
|
||||||
|
"@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="],
|
||||||
|
|
||||||
|
"@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="],
|
||||||
|
|
||||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||||
|
|
||||||
"@qdrant/js-client-rest": ["@qdrant/js-client-rest@1.17.0", "", { "dependencies": { "@qdrant/openapi-typescript-fetch": "1.2.6", "undici": "6.24.1" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A=="],
|
"@qdrant/js-client-rest": ["@qdrant/js-client-rest@1.17.0", "", { "dependencies": { "@qdrant/openapi-typescript-fetch": "1.2.6", "undici": "6.24.1" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A=="],
|
||||||
|
|
@ -303,6 +398,8 @@
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "2.3.5", "enhanced-resolve": "5.20.1", "jiti": "2.6.1", "lightningcss": "1.32.0", "magic-string": "0.30.21", "source-map-js": "1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "2.3.5", "enhanced-resolve": "5.20.1", "jiti": "2.6.1", "lightningcss": "1.32.0", "magic-string": "0.30.21", "source-map-js": "1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
|
||||||
|
|
@ -371,6 +468,8 @@
|
||||||
|
|
||||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "7.29.0" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "7.29.0" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
||||||
|
|
||||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "4.0.2", "assertion-error": "2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "4.0.2", "assertion-error": "2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "2.1.0" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "2.1.0" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||||
|
|
@ -387,7 +486,7 @@
|
||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
|
"@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="],
|
||||||
|
|
||||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "22.19.17", "form-data": "4.0.5" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "22.19.17", "form-data": "4.0.5" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||||
|
|
||||||
|
|
@ -401,23 +500,39 @@
|
||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "22.19.17" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "22.19.17" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260511.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260511.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260511.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260511.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260511.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260511.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260511.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260511.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-cUyY4Sr6065280lB6hCwTMCBMTxlEIGjSLzHym28yikA5sFiEsAzlwiU0i+XkTUIqr5K5M/SzSJiioDN+vpjtA=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260511.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SYrqVOlapDxDG7FzHBIJbfgaix+mXPkYzYGqwpz/TAhoPA7sgbfAoGLaqi3ut9N88C/OYNhEX4tjz/0PC9i1nw=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260511.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zIe31OYgBvkgTIQEwJtKim6SYyuVTkr+9fK/87hVwKN15X3Ikjeh0C0g2W/Vl4rXeMvy95wBGDN1jpW11DIvgg=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260511.1", "", { "os": "linux", "cpu": "arm" }, "sha512-02b45lpPmYf125PvcnK67WW93N55qwKmtInwfVefV997S17Ib3h6hlCW4e24BDhNsGRCSLhPA4Lu7ZvTq5pLkw=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260511.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-YbmCQXGYkDChGFG7hXJzIgmRjtU1kE5VK/+k322nGnbq4ePqSjS3dS0+ehPATmvfO1XjCDfh3ekED+AtmWk6aQ=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260511.1", "", { "os": "linux", "cpu": "x64" }, "sha512-e+TweaVJFaM96tV1UM1kRfk2y8QBkZtz7+0wcxrDGmyJz3IIRUlg1btocaBkhsmVtQPXMr37RutBBMgpl3vgUg=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260511.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-zgkoGiCpOrly5h8ghcuu6ZNSfrnRqtHoCq584Q92+s4D/j1MU3oKkGPvmkezp5Mj2v7ffR9AjU+lWRDkrfm6eA=="],
|
||||||
|
|
||||||
|
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260511.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SUm7iVYzKaflol+QwH0Ny5jZtco6PJduI+h/TEg0sgBJzVBa+9RN4I9+Xu9v+EJ1bci3XI7835IRdSP36lCgCw=="],
|
||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "7.29.0", "@babel/plugin-transform-react-jsx-self": "7.27.1", "@babel/plugin-transform-react-jsx-source": "7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "7.20.5", "react-refresh": "0.17.0" }, "peerDependencies": { "vite": "6.4.1" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "7.29.0", "@babel/plugin-transform-react-jsx-self": "7.27.1", "@babel/plugin-transform-react-jsx-source": "7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "7.20.5", "react-refresh": "0.17.0" }, "peerDependencies": { "vite": "6.4.1" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||||
|
|
||||||
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "5.2.3", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "5.3.3", "tinyrainbow": "2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
"@vitest/expect": ["@vitest/expect@4.1.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg=="],
|
||||||
|
|
||||||
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "3.0.3", "magic-string": "0.30.21" }, "optionalDependencies": { "vite": "6.4.1" } }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
|
"@vitest/mocker": ["@vitest/mocker@4.1.6", "", { "dependencies": { "@vitest/spy": "4.1.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ=="],
|
||||||
|
|
||||||
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.6", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw=="],
|
||||||
|
|
||||||
"@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "2.0.3", "strip-literal": "3.1.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
|
"@vitest/runner": ["@vitest/runner@4.1.6", "", { "dependencies": { "@vitest/utils": "4.1.6", "pathe": "^2.0.3" } }, "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA=="],
|
||||||
|
|
||||||
"@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "0.30.21", "pathe": "2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
|
"@vitest/snapshot": ["@vitest/snapshot@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "@vitest/utils": "4.1.6", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw=="],
|
||||||
|
|
||||||
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "4.0.4" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
|
"@vitest/spy": ["@vitest/spy@4.1.6", "", {}, "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg=="],
|
||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "3.2.1", "tinyrainbow": "2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
|
"@vitest/utils": ["@vitest/utils@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ=="],
|
||||||
|
|
||||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "5.0.1" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "5.0.1" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||||
|
|
||||||
|
|
@ -453,9 +568,9 @@
|
||||||
|
|
||||||
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "2.10.15", "caniuse-lite": "1.0.30001785", "electron-to-chromium": "1.5.331", "node-releases": "2.0.37", "update-browserslist-db": "1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "2.10.15", "caniuse-lite": "1.0.30001785", "electron-to-chromium": "1.5.331", "node-releases": "2.0.37", "update-browserslist-db": "1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||||
|
|
||||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
||||||
|
|
||||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "1.3.0", "function-bind": "1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "1.3.0", "function-bind": "1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
|
@ -467,7 +582,7 @@
|
||||||
|
|
||||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||||
|
|
||||||
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "2.0.1", "check-error": "2.1.3", "deep-eql": "5.0.2", "loupe": "3.2.1", "pathval": "2.0.1" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||||
|
|
||||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||||
|
|
||||||
|
|
@ -477,10 +592,6 @@
|
||||||
|
|
||||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||||
|
|
||||||
"check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
|
|
||||||
|
|
||||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||||
|
|
@ -549,8 +660,6 @@
|
||||||
|
|
||||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "2.0.2" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "2.0.2" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||||
|
|
||||||
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
|
||||||
|
|
||||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
@ -567,6 +676,8 @@
|
||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
|
"effect": ["effect@4.0.0-beta.65", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-QYKvQPAj3CmtsvWkHQww15wX4KG2gNsszDWEcOO5sZCMknp66u6Si/Opmt3wwWCwsyvRmDAdIg+JIz5qzbbFIw=="],
|
||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.331", "", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.331", "", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="],
|
||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
@ -581,7 +692,7 @@
|
||||||
|
|
||||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
"es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
|
||||||
|
|
||||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
|
@ -615,6 +726,8 @@
|
||||||
|
|
||||||
"falkordb": ["falkordb@5.0.1", "", { "dependencies": { "@falkordb/client": "1.6.0", "@falkordb/graph": "2.0.1" } }, "sha512-cG/reBmAF5DJx0HnkLF+tV1deTcufx+dBEbYl3gZmBiZmlWk3YYM1IDcEA8ZeAm1Zw6NxaEjoqepwZNYedTm3Q=="],
|
"falkordb": ["falkordb@5.0.1", "", { "dependencies": { "@falkordb/client": "1.6.0", "@falkordb/graph": "2.0.1" } }, "sha512-cG/reBmAF5DJx0HnkLF+tV1deTcufx+dBEbYl3gZmBiZmlWk3YYM1IDcEA8ZeAm1Zw6NxaEjoqepwZNYedTm3Q=="],
|
||||||
|
|
||||||
|
"fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
|
||||||
|
|
||||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
@ -637,6 +750,8 @@
|
||||||
|
|
||||||
"find-my-way": ["find-my-way@9.5.0", "", { "dependencies": { "fast-deep-equal": "3.1.3", "fast-querystring": "1.1.2", "safe-regex2": "5.1.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="],
|
"find-my-way": ["find-my-way@9.5.0", "", { "dependencies": { "fast-deep-equal": "3.1.3", "fast-querystring": "1.1.2", "safe-regex2": "5.1.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="],
|
||||||
|
|
||||||
|
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
|
||||||
|
|
||||||
"float-tooltip": ["float-tooltip@1.7.5", "", { "dependencies": { "d3-selection": "3.0.0", "kapsule": "1.16.3", "preact": "10.29.1" } }, "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg=="],
|
"float-tooltip": ["float-tooltip@1.7.5", "", { "dependencies": { "d3-selection": "3.0.0", "kapsule": "1.16.3", "preact": "10.29.1" } }, "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg=="],
|
||||||
|
|
||||||
"force-graph": ["force-graph@1.51.2", "", { "dependencies": { "@tweenjs/tween.js": "25.0.0", "accessor-fn": "1.5.3", "bezier-js": "6.1.4", "canvas-color-tracker": "1.3.2", "d3-array": "3.2.4", "d3-drag": "3.0.0", "d3-force-3d": "3.0.6", "d3-scale": "4.0.2", "d3-scale-chromatic": "3.1.0", "d3-selection": "3.0.0", "d3-zoom": "3.0.0", "float-tooltip": "1.7.5", "index-array-by": "1.4.2", "kapsule": "1.16.3", "lodash-es": "4.18.1" } }, "sha512-zZNdMqx8qIQGurgnbgYIUsdXxSfvhfRSIdncsKGv/twUOZpwCsk9hPHmdjdcme1+epATgb41G0rkIGHJ0Wydng=="],
|
"force-graph": ["force-graph@1.51.2", "", { "dependencies": { "@tweenjs/tween.js": "25.0.0", "accessor-fn": "1.5.3", "bezier-js": "6.1.4", "canvas-color-tracker": "1.3.2", "d3-array": "3.2.4", "d3-drag": "3.0.0", "d3-force-3d": "3.0.6", "d3-scale": "4.0.2", "d3-scale-chromatic": "3.1.0", "d3-selection": "3.0.0", "d3-zoom": "3.0.0", "float-tooltip": "1.7.5", "index-array-by": "1.4.2", "kapsule": "1.16.3", "lodash-es": "4.18.1" } }, "sha512-zZNdMqx8qIQGurgnbgYIUsdXxSfvhfRSIdncsKGv/twUOZpwCsk9hPHmdjdcme1+epATgb41G0rkIGHJ0Wydng=="],
|
||||||
|
|
@ -695,6 +810,8 @@
|
||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
|
||||||
|
|
||||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||||
|
|
||||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||||
|
|
@ -723,7 +840,7 @@
|
||||||
|
|
||||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
|
@ -737,6 +854,8 @@
|
||||||
|
|
||||||
"kapsule": ["kapsule@1.16.3", "", { "dependencies": { "lodash-es": "4.18.1" } }, "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg=="],
|
"kapsule": ["kapsule@1.16.3", "", { "dependencies": { "lodash-es": "4.18.1" } }, "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg=="],
|
||||||
|
|
||||||
|
"kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
|
||||||
|
|
||||||
"light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "1.1.1", "process-warning": "4.0.1", "set-cookie-parser": "2.7.2" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
|
"light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "1.1.1", "process-warning": "4.0.1", "set-cookie-parser": "2.7.2" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
@ -769,8 +888,6 @@
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "3.1.1" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "3.1.1" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.513.0", "", { "peerDependencies": { "react": "19.2.4" } }, "sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg=="],
|
"lucide-react": ["lucide-react@0.513.0", "", { "peerDependencies": { "react": "19.2.4" } }, "sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg=="],
|
||||||
|
|
@ -847,6 +964,12 @@
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"msgpackr": ["msgpackr@1.11.12", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg=="],
|
||||||
|
|
||||||
|
"msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="],
|
||||||
|
|
||||||
|
"multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
"nats": ["nats@2.29.3", "", { "dependencies": { "nkeys.js": "1.1.0" } }, "sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA=="],
|
"nats": ["nats@2.29.3", "", { "dependencies": { "nkeys.js": "1.1.0" } }, "sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA=="],
|
||||||
|
|
@ -859,12 +982,18 @@
|
||||||
|
|
||||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "5.0.0" } }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "5.0.0" } }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
|
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
||||||
|
|
||||||
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||||
|
|
||||||
|
"ollama": ["ollama@0.6.3", "", { "dependencies": { "whatwg-fetch": "^3.6.20" } }, "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg=="],
|
||||||
|
|
||||||
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||||
|
|
||||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
|
|
@ -873,6 +1002,8 @@
|
||||||
|
|
||||||
"openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "18.19.130", "@types/node-fetch": "2.6.13", "abort-controller": "3.0.0", "agentkeepalive": "4.6.0", "form-data-encoder": "1.7.2", "formdata-node": "4.4.1", "node-fetch": "2.7.0" }, "optionalDependencies": { "ws": "8.20.0", "zod": "3.25.76" }, "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
|
"openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "18.19.130", "@types/node-fetch": "2.6.13", "abort-controller": "3.0.0", "agentkeepalive": "4.6.0", "form-data-encoder": "1.7.2", "formdata-node": "4.4.1", "node-fetch": "2.7.0" }, "optionalDependencies": { "ws": "8.20.0", "zod": "3.25.76" }, "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
|
||||||
|
|
||||||
|
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "2.0.11", "character-entities-legacy": "3.0.0", "character-reference-invalid": "2.0.1", "decode-named-character-reference": "1.3.0", "is-alphanumerical": "2.0.1", "is-decimal": "2.0.1", "is-hexadecimal": "2.0.1" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "2.0.11", "character-entities-legacy": "3.0.0", "character-reference-invalid": "2.0.1", "decode-named-character-reference": "1.3.0", "is-alphanumerical": "2.0.1", "is-decimal": "2.0.1", "is-hexadecimal": "2.0.1" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||||
|
|
||||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
|
|
@ -883,7 +1014,9 @@
|
||||||
|
|
||||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
|
"pdf-lib": ["pdf-lib@1.17.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "pako": "^1.0.11", "tslib": "^1.11.1" } }, "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw=="],
|
||||||
|
|
||||||
|
"pdfjs-dist": ["pdfjs-dist@5.7.284", "", { "optionalDependencies": { "@napi-rs/canvas": "^0.1.100" } }, "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw=="],
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
|
@ -911,6 +1044,8 @@
|
||||||
|
|
||||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
|
"pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="],
|
||||||
|
|
||||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||||
|
|
||||||
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||||
|
|
@ -1005,7 +1140,7 @@
|
||||||
|
|
||||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
"std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="],
|
||||||
|
|
||||||
"stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="],
|
"stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="],
|
||||||
|
|
||||||
|
|
@ -1013,8 +1148,6 @@
|
||||||
|
|
||||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "2.1.0", "character-entities-legacy": "3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "2.1.0", "character-entities-legacy": "3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||||
|
|
||||||
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
|
|
||||||
|
|
||||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||||
|
|
||||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||||
|
|
@ -1033,26 +1166,26 @@
|
||||||
|
|
||||||
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
|
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
"tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.4" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.4" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
|
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
|
||||||
|
|
||||||
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
|
|
||||||
|
|
||||||
"tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
|
|
||||||
|
|
||||||
"toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="],
|
"toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="],
|
||||||
|
|
||||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
|
"toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
|
||||||
|
|
||||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
|
|
||||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
|
||||||
|
|
||||||
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "0.27.7", "get-tsconfig": "4.13.7" }, "optionalDependencies": { "fsevents": "2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "0.27.7", "get-tsconfig": "4.13.7" }, "optionalDependencies": { "fsevents": "2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
||||||
|
|
||||||
"turbo": ["turbo@2.9.4", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.4", "@turbo/darwin-arm64": "2.9.4", "@turbo/linux-64": "2.9.4", "@turbo/linux-arm64": "2.9.4", "@turbo/windows-64": "2.9.4", "@turbo/windows-arm64": "2.9.4" }, "bin": { "turbo": "bin/turbo" } }, "sha512-wZ/kMcZCuK5oEp7sXSSo/5fzKjP9I2EhoiarZjyCm2Ixk0WxFrC/h0gF3686eHHINoFQOOSWgB/pGfvkR8rkgQ=="],
|
"turbo": ["turbo@2.9.4", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.4", "@turbo/darwin-arm64": "2.9.4", "@turbo/linux-64": "2.9.4", "@turbo/linux-arm64": "2.9.4", "@turbo/windows-64": "2.9.4", "@turbo/windows-arm64": "2.9.4" }, "bin": { "turbo": "bin/turbo" } }, "sha512-wZ/kMcZCuK5oEp7sXSSo/5fzKjP9I2EhoiarZjyCm2Ixk0WxFrC/h0gF3686eHHINoFQOOSWgB/pGfvkR8rkgQ=="],
|
||||||
|
|
@ -1065,7 +1198,7 @@
|
||||||
|
|
||||||
"undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
"undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="],
|
||||||
|
|
||||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "3.0.3", "bail": "2.0.2", "devlop": "1.1.0", "extend": "3.0.2", "is-plain-obj": "4.1.0", "trough": "2.2.0", "vfile": "6.0.3" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "3.0.3", "bail": "2.0.2", "devlop": "1.1.0", "extend": "3.0.2", "is-plain-obj": "4.1.0", "trough": "2.2.0", "vfile": "6.0.3" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||||
|
|
||||||
|
|
@ -1085,6 +1218,8 @@
|
||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"uuid": ["uuid@13.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="],
|
||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "3.0.3", "vfile-message": "4.0.3" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "3.0.3", "vfile-message": "4.0.3" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
@ -1093,14 +1228,14 @@
|
||||||
|
|
||||||
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "0.25.12", "fdir": "6.5.0", "picomatch": "4.0.4", "postcss": "8.5.8", "rollup": "4.60.1", "tinyglobby": "0.2.15" }, "optionalDependencies": { "@types/node": "22.19.17", "fsevents": "2.3.3", "jiti": "2.6.1", "lightningcss": "1.32.0", "tsx": "4.21.0" }, "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "0.25.12", "fdir": "6.5.0", "picomatch": "4.0.4", "postcss": "8.5.8", "rollup": "4.60.1", "tinyglobby": "0.2.15" }, "optionalDependencies": { "@types/node": "22.19.17", "fsevents": "2.3.3", "jiti": "2.6.1", "lightningcss": "1.32.0", "tsx": "4.21.0" }, "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||||
|
|
||||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "6.7.14", "debug": "4.4.3", "es-module-lexer": "1.7.0", "pathe": "2.0.3", "vite": "6.4.1" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
"vitest": ["vitest@4.1.6", "", { "dependencies": { "@vitest/expect": "4.1.6", "@vitest/mocker": "4.1.6", "@vitest/pretty-format": "4.1.6", "@vitest/runner": "4.1.6", "@vitest/snapshot": "4.1.6", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.6", "@vitest/browser-preview": "4.1.6", "@vitest/browser-webdriverio": "4.1.6", "@vitest/coverage-istanbul": "4.1.6", "@vitest/coverage-v8": "4.1.6", "@vitest/ui": "4.1.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ=="],
|
||||||
|
|
||||||
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "5.2.3", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "5.3.3", "debug": "4.4.3", "expect-type": "1.3.0", "magic-string": "0.30.21", "pathe": "2.0.3", "picomatch": "4.0.4", "std-env": "3.10.0", "tinybench": "2.9.0", "tinyexec": "0.3.2", "tinyglobby": "0.2.15", "tinypool": "1.1.1", "tinyrainbow": "2.0.0", "vite": "6.4.1", "vite-node": "3.2.4", "why-is-node-running": "2.3.0" }, "optionalDependencies": { "@types/debug": "4.1.13", "@types/node": "22.19.17", "happy-dom": "20.8.9" }, "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
|
|
||||||
|
|
||||||
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
||||||
|
|
||||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
|
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
||||||
|
|
||||||
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
||||||
|
|
||||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "0.0.3", "webidl-conversions": "3.0.1" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "0.0.3", "webidl-conversions": "3.0.1" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
|
@ -1115,6 +1250,8 @@
|
||||||
|
|
||||||
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
|
||||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "3.25.76" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "3.25.76" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||||
|
|
@ -1125,19 +1262,29 @@
|
||||||
|
|
||||||
"@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "5.26.5" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
"@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "5.26.5" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||||
|
|
||||||
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
|
||||||
|
|
||||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"@trustgraph/base/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
|
||||||
|
|
||||||
|
"@trustgraph/client/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
|
||||||
|
|
||||||
|
"@trustgraph/flow/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
|
||||||
|
|
||||||
|
"@trustgraph/mcp/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
|
||||||
|
|
||||||
|
"@types/node-fetch/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
|
||||||
|
|
||||||
|
"@types/ws/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
|
||||||
|
|
||||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
"happy-dom/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
|
||||||
|
|
||||||
"loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
||||||
|
|
||||||
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
|
@ -1147,14 +1294,32 @@
|
||||||
|
|
||||||
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
|
"vite/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
|
||||||
|
|
||||||
"vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
"vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||||
|
|
||||||
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
|
"@trustgraph/base/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"@trustgraph/client/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"@trustgraph/flow/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"@trustgraph/mcp/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"@types/node-fetch/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"@types/ws/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
|
"vite/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||||
|
|
||||||
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ services:
|
||||||
|
|
||||||
# Override text-completion to use Ollama (no API key needed for local dev)
|
# Override text-completion to use Ollama (no API key needed for local dev)
|
||||||
text-completion:
|
text-completion:
|
||||||
command: ["node", "entrypoints/text-completion-ollama.mjs"]
|
command: ["bun", "entrypoints/text-completion-ollama.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
- OLLAMA_URL=http://ollama:11434
|
- OLLAMA_URL=http://ollama:11434
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ services:
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: Containerfile
|
dockerfile: Containerfile
|
||||||
command: ["node", "entrypoints/gateway.mjs"]
|
command: ["bun", "entrypoints/gateway.mjs"]
|
||||||
ports:
|
ports:
|
||||||
- "${GATEWAY_PORT:-8088}:8088"
|
- "${GATEWAY_PORT:-8088}:8088"
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -227,7 +227,7 @@ services:
|
||||||
|
|
||||||
config-service:
|
config-service:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/config.mjs"]
|
command: ["bun", "entrypoints/config.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -239,7 +239,7 @@ services:
|
||||||
|
|
||||||
text-completion:
|
text-completion:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/text-completion-openai.mjs"]
|
command: ["bun", "entrypoints/text-completion-openai.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
- OPENAI_TOKEN=${OPENAI_TOKEN:-}
|
- OPENAI_TOKEN=${OPENAI_TOKEN:-}
|
||||||
|
|
@ -253,7 +253,7 @@ services:
|
||||||
|
|
||||||
prompt:
|
prompt:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/prompt.mjs"]
|
command: ["bun", "entrypoints/prompt.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -265,7 +265,7 @@ services:
|
||||||
|
|
||||||
embeddings:
|
embeddings:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/embeddings.mjs"]
|
command: ["bun", "entrypoints/embeddings.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
- OLLAMA_URL=http://ollama:11434
|
- OLLAMA_URL=http://ollama:11434
|
||||||
|
|
@ -292,7 +292,7 @@ services:
|
||||||
|
|
||||||
agent:
|
agent:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/agent.mjs"]
|
command: ["bun", "entrypoints/agent.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -304,7 +304,7 @@ services:
|
||||||
|
|
||||||
mcp-tool:
|
mcp-tool:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/mcp-tool.mjs"]
|
command: ["bun", "entrypoints/mcp-tool.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -316,7 +316,7 @@ services:
|
||||||
|
|
||||||
librarian:
|
librarian:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/librarian.mjs"]
|
command: ["bun", "entrypoints/librarian.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -330,7 +330,7 @@ services:
|
||||||
|
|
||||||
flow-manager:
|
flow-manager:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/flow-manager.mjs"]
|
command: ["bun", "entrypoints/flow-manager.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -342,7 +342,7 @@ services:
|
||||||
|
|
||||||
knowledge-cores:
|
knowledge-cores:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/cores.mjs"]
|
command: ["bun", "entrypoints/cores.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -358,7 +358,7 @@ services:
|
||||||
|
|
||||||
pdf-decoder:
|
pdf-decoder:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/pdf-decoder.mjs"]
|
command: ["bun", "entrypoints/pdf-decoder.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -370,7 +370,7 @@ services:
|
||||||
|
|
||||||
chunker:
|
chunker:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/chunker.mjs"]
|
command: ["bun", "entrypoints/chunker.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -382,7 +382,7 @@ services:
|
||||||
|
|
||||||
extractor:
|
extractor:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/extractor.mjs"]
|
command: ["bun", "entrypoints/extractor.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -394,7 +394,7 @@ services:
|
||||||
|
|
||||||
triples-store:
|
triples-store:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/triples-store.mjs"]
|
command: ["bun", "entrypoints/triples-store.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
- FALKORDB_URL=redis://falkordb:6379
|
- FALKORDB_URL=redis://falkordb:6379
|
||||||
|
|
@ -409,7 +409,7 @@ services:
|
||||||
|
|
||||||
graph-embeddings-store:
|
graph-embeddings-store:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/graph-embeddings-store.mjs"]
|
command: ["bun", "entrypoints/graph-embeddings-store.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
- QDRANT_URL=http://qdrant:6333
|
- QDRANT_URL=http://qdrant:6333
|
||||||
|
|
@ -428,7 +428,7 @@ services:
|
||||||
|
|
||||||
triples-query:
|
triples-query:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/triples-query.mjs"]
|
command: ["bun", "entrypoints/triples-query.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
- FALKORDB_URL=redis://falkordb:6379
|
- FALKORDB_URL=redis://falkordb:6379
|
||||||
|
|
@ -443,7 +443,7 @@ services:
|
||||||
|
|
||||||
graph-embeddings-query:
|
graph-embeddings-query:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/graph-embeddings-query.mjs"]
|
command: ["bun", "entrypoints/graph-embeddings-query.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
- QDRANT_URL=http://qdrant:6333
|
- QDRANT_URL=http://qdrant:6333
|
||||||
|
|
@ -458,7 +458,7 @@ services:
|
||||||
|
|
||||||
doc-embeddings-query:
|
doc-embeddings-query:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/doc-embeddings-query.mjs"]
|
command: ["bun", "entrypoints/doc-embeddings-query.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
- QDRANT_URL=http://qdrant:6333
|
- QDRANT_URL=http://qdrant:6333
|
||||||
|
|
@ -477,7 +477,7 @@ services:
|
||||||
|
|
||||||
graph-rag:
|
graph-rag:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/graph-rag.mjs"]
|
command: ["bun", "entrypoints/graph-rag.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -489,7 +489,7 @@ services:
|
||||||
|
|
||||||
document-rag:
|
document-rag:
|
||||||
image: trustgraph-ts:local
|
image: trustgraph-ts:local
|
||||||
command: ["node", "entrypoints/document-rag.mjs"]
|
command: ["bun", "entrypoints/document-rag.mjs"]
|
||||||
environment:
|
environment:
|
||||||
- NATS_URL=nats://nats:4222
|
- NATS_URL=nats://nats:4222
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -505,7 +505,7 @@ services:
|
||||||
|
|
||||||
# text-completion-ollama:
|
# text-completion-ollama:
|
||||||
# image: trustgraph-ts:local
|
# image: trustgraph-ts:local
|
||||||
# command: ["node", "entrypoints/text-completion-ollama.mjs"]
|
# command: ["bun", "entrypoints/text-completion-ollama.mjs"]
|
||||||
# environment:
|
# environment:
|
||||||
# - NATS_URL=nats://nats:4222
|
# - NATS_URL=nats://nats:4222
|
||||||
# - OLLAMA_URL=http://ollama:11434
|
# - OLLAMA_URL=http://ollama:11434
|
||||||
|
|
@ -521,7 +521,7 @@ services:
|
||||||
|
|
||||||
# text-completion-claude:
|
# text-completion-claude:
|
||||||
# image: trustgraph-ts:local
|
# image: trustgraph-ts:local
|
||||||
# command: ["node", "entrypoints/text-completion-claude.mjs"]
|
# command: ["bun", "entrypoints/text-completion-claude.mjs"]
|
||||||
# environment:
|
# environment:
|
||||||
# - NATS_URL=nats://nats:4222
|
# - NATS_URL=nats://nats:4222
|
||||||
# - CLAUDE_KEY=${CLAUDE_KEY:-}
|
# - CLAUDE_KEY=${CLAUDE_KEY:-}
|
||||||
|
|
@ -534,7 +534,7 @@ services:
|
||||||
|
|
||||||
# text-completion-azure-openai:
|
# text-completion-azure-openai:
|
||||||
# image: trustgraph-ts:local
|
# image: trustgraph-ts:local
|
||||||
# command: ["node", "entrypoints/text-completion-azure-openai.mjs"]
|
# command: ["bun", "entrypoints/text-completion-azure-openai.mjs"]
|
||||||
# environment:
|
# environment:
|
||||||
# - NATS_URL=nats://nats:4222
|
# - NATS_URL=nats://nats:4222
|
||||||
# - AZURE_TOKEN=${AZURE_TOKEN:-}
|
# - AZURE_TOKEN=${AZURE_TOKEN:-}
|
||||||
|
|
@ -550,7 +550,7 @@ services:
|
||||||
|
|
||||||
# text-completion-openai-compatible:
|
# text-completion-openai-compatible:
|
||||||
# image: trustgraph-ts:local
|
# image: trustgraph-ts:local
|
||||||
# command: ["node", "entrypoints/text-completion-openai-compatible.mjs"]
|
# command: ["bun", "entrypoints/text-completion-openai-compatible.mjs"]
|
||||||
# environment:
|
# environment:
|
||||||
# - NATS_URL=nats://nats:4222
|
# - NATS_URL=nats://nats:4222
|
||||||
# - OPENAI_COMPAT_URL=${OPENAI_COMPAT_URL:-http://localhost:1234/v1}
|
# - OPENAI_COMPAT_URL=${OPENAI_COMPAT_URL:-http://localhost:1234/v1}
|
||||||
|
|
@ -565,7 +565,7 @@ services:
|
||||||
|
|
||||||
# text-completion-mistral:
|
# text-completion-mistral:
|
||||||
# image: trustgraph-ts:local
|
# image: trustgraph-ts:local
|
||||||
# command: ["node", "entrypoints/text-completion-mistral.mjs"]
|
# command: ["bun", "entrypoints/text-completion-mistral.mjs"]
|
||||||
# environment:
|
# environment:
|
||||||
# - NATS_URL=nats://nats:4222
|
# - NATS_URL=nats://nats:4222
|
||||||
# - MISTRAL_TOKEN=${MISTRAL_TOKEN:-}
|
# - MISTRAL_TOKEN=${MISTRAL_TOKEN:-}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
// Will work once the agent service is merged.
|
// Will work once the agent service is merged.
|
||||||
import("../packages/flow/dist/agent/react/service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/agent/react/service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/chunking/service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/chunking/service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/config/service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/config/service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/cores/service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/cores/service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/query/embeddings/qdrant-doc-service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/query/embeddings/qdrant-doc-service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/retrieval/document-rag-service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/retrieval/document-rag-service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/embeddings/ollama.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/embeddings/ollama.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/extract/knowledge-extract.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/extract/knowledge-extract.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/flow-manager/service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/flow-manager/service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/gateway/server.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/gateway/server.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/query/embeddings/qdrant-graph-service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/query/embeddings/qdrant-graph-service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/storage/embeddings/graph-embeddings-service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/storage/embeddings/graph-embeddings-service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/retrieval/graph-rag-service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/retrieval/graph-rag-service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
// Will work once the librarian service is merged.
|
// Will work once the librarian service is merged.
|
||||||
import("../packages/flow/dist/librarian/service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/librarian/service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/agent/mcp-tool/service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.McpToolService.launch("mcp-tool"))
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/agent/mcp-tool/service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/decoding/pdf-decoder.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/decoding/pdf-decoder.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/prompt/template.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/prompt/template.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/model/text-completion/azure-openai.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/model/text-completion/azure-openai.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/model/text-completion/claude.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/model/text-completion/claude.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/model/text-completion/mistral.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/model/text-completion/mistral.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/model/text-completion/ollama.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/model/text-completion/ollama.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/model/text-completion/openai-compatible.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/model/text-completion/openai-compatible.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/model/text-completion/openai.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/model/text-completion/openai.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/query/triples/falkordb-service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/query/triples/falkordb-service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import("../packages/flow/dist/storage/triples/falkordb-service.js")
|
import("@effect/platform-bun/BunRuntime")
|
||||||
.then((m) => m.run())
|
.then(async (BunRuntime) => {
|
||||||
|
const m = await import("../packages/flow/dist/storage/triples/falkordb-service.js");
|
||||||
|
BunRuntime.runMain(m.program);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -2,50 +2,62 @@
|
||||||
"name": "trustgraph-ts",
|
"name": "trustgraph-ts",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo build",
|
"build": "bunx --bun turbo build",
|
||||||
"dev": "turbo dev",
|
"dev": "bunx --bun turbo dev",
|
||||||
"lint": "turbo lint",
|
"lint": "bunx --bun turbo lint",
|
||||||
"test": "turbo test",
|
"test": "bunx --bun turbo test",
|
||||||
|
"check:tsgo": "tsgo -b tsconfig.json",
|
||||||
|
"prepare": "effect-tsgo patch",
|
||||||
"clean": "turbo clean",
|
"clean": "turbo clean",
|
||||||
"gateway": "tsx scripts/run-gateway.ts",
|
"gateway": "bun scripts/run-gateway.ts",
|
||||||
"config-svc": "tsx scripts/run-config.ts",
|
"config-svc": "bun scripts/run-config.ts",
|
||||||
"llm:claude": "tsx scripts/run-llm-claude.ts",
|
"llm:claude": "bun scripts/run-llm-claude.ts",
|
||||||
"llm:openai": "tsx scripts/run-llm-openai.ts",
|
"llm:openai": "bun scripts/run-llm-openai.ts",
|
||||||
"test:pipeline": "tsx scripts/test-pipeline.ts",
|
"test:pipeline": "bun scripts/test-pipeline.ts",
|
||||||
"seed": "tsx scripts/seed-config.ts",
|
"seed": "bun scripts/seed-config.ts",
|
||||||
"prompt": "tsx scripts/run-prompt.ts",
|
"prompt": "bun scripts/run-prompt.ts",
|
||||||
"agent": "tsx scripts/run-agent.ts",
|
"agent": "bun scripts/run-agent.ts",
|
||||||
"librarian": "tsx scripts/run-librarian.ts",
|
"librarian": "bun scripts/run-librarian.ts",
|
||||||
"knowledge": "tsx scripts/run-knowledge.ts",
|
"knowledge": "bun scripts/run-knowledge.ts",
|
||||||
"flow-manager": "tsx scripts/run-flow-manager.ts",
|
"flow-manager": "bun scripts/run-flow-manager.ts",
|
||||||
"llm:ollama": "tsx scripts/run-ollama.ts",
|
"llm:ollama": "bun scripts/run-ollama.ts",
|
||||||
"pdf-decoder": "tsx scripts/run-pdf-decoder.ts",
|
"pdf-decoder": "bun scripts/run-pdf-decoder.ts",
|
||||||
"triples-store": "tsx scripts/run-triples-store.ts",
|
"triples-store": "bun scripts/run-triples-store.ts",
|
||||||
"graph-embeddings-store": "tsx scripts/run-graph-embeddings-store.ts",
|
"graph-embeddings-store": "bun scripts/run-graph-embeddings-store.ts",
|
||||||
"chunker": "tsx scripts/run-chunker.ts",
|
"chunker": "bun scripts/run-chunker.ts",
|
||||||
"extractor": "tsx scripts/run-extractor.ts",
|
"extractor": "bun scripts/run-extractor.ts",
|
||||||
"embeddings": "tsx scripts/run-embeddings.ts",
|
"embeddings": "bun scripts/run-embeddings.ts",
|
||||||
"triples-query": "tsx scripts/run-triples-query.ts",
|
"triples-query": "bun scripts/run-triples-query.ts",
|
||||||
"graph-embeddings-query": "tsx scripts/run-graph-embeddings-query.ts",
|
"graph-embeddings-query": "bun scripts/run-graph-embeddings-query.ts",
|
||||||
"doc-embeddings-query": "tsx scripts/run-doc-embeddings-query.ts",
|
"doc-embeddings-query": "bun scripts/run-doc-embeddings-query.ts",
|
||||||
"graph-rag": "tsx scripts/run-graph-rag.ts",
|
"graph-rag": "bun scripts/run-graph-rag.ts",
|
||||||
"document-rag": "tsx scripts/run-document-rag.ts",
|
"document-rag": "bun scripts/run-document-rag.ts",
|
||||||
"create-test-pdf": "tsx scripts/create-test-pdf.ts",
|
"create-test-pdf": "bun scripts/create-test-pdf.ts",
|
||||||
"seed:demo": "tsx scripts/seed-demo.ts",
|
"seed:demo": "bun scripts/seed-demo.ts",
|
||||||
"mcp-tool": "tsx scripts/run-mcp-tool.ts",
|
"mcp-tool": "bun scripts/run-mcp-tool.ts",
|
||||||
"llm:azure-openai": "tsx scripts/run-llm-azure-openai.ts",
|
"llm:azure-openai": "bun scripts/run-llm-azure-openai.ts",
|
||||||
"llm:openai-compat": "tsx scripts/run-llm-openai-compatible.ts",
|
"llm:openai-compat": "bun scripts/run-llm-openai-compatible.ts",
|
||||||
"llm:mistral": "tsx scripts/run-llm-mistral.ts"
|
"llm:mistral": "bun scripts/run-llm-mistral.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/platform-bun": "4.0.0-beta.65",
|
||||||
|
"@effect/tsgo": "0.6.0",
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
|
"@types/bun": "^1.3.13",
|
||||||
|
"@types/node": "^25.7.0",
|
||||||
|
"@typescript/native-preview": "^7.0.0-dev.20260511.1",
|
||||||
"falkordb": "^5.0.0",
|
"falkordb": "^5.0.0",
|
||||||
"nats": "^2.29.0",
|
"nats": "^2.29.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"turbo": "^2.5.0",
|
"turbo": "^2.5.0",
|
||||||
"typescript": "^5.8.0"
|
"typescript": "^5.8.0",
|
||||||
|
"vitest": "^4.1.6"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.15.0",
|
"dependencies": {
|
||||||
|
"effect": "4.0.0-beta.65"
|
||||||
|
},
|
||||||
|
"packageManager": "bun@1.3.13",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,30 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./backend": "./src/backend/index.ts",
|
||||||
|
"./messaging": "./src/messaging/index.ts",
|
||||||
|
"./processor": "./src/processor/index.ts",
|
||||||
|
"./runtime": "./src/runtime/index.ts",
|
||||||
|
"./schema": "./src/schema/index.ts",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "bunx --bun tsc",
|
||||||
"dev": "tsc --watch",
|
"dev": "tsc --watch",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"test": "vitest run"
|
"test": "bunx --bun vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"effect": "4.0.0-beta.65",
|
||||||
"nats": "^2.29.0",
|
"nats": "^2.29.0",
|
||||||
"prom-client": "^15.1.0"
|
"prom-client": "^15.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^3.1.0"
|
"vitest": "^4.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
CreateProducerOptions,
|
CreateProducerOptions,
|
||||||
CreateConsumerOptions,
|
CreateConsumerOptions,
|
||||||
} from "../backend/types.js";
|
} from "../backend/types.js";
|
||||||
import { TooManyRequestsError } from "../errors.js";
|
import { tooManyRequestsError } from "../errors.js";
|
||||||
import type { Flow } from "../processor/flow.js";
|
import type { Flow } from "../processor/flow.js";
|
||||||
|
|
||||||
// ── Mock Message ──────────────────────────────────────────────────────
|
// ── Mock Message ──────────────────────────────────────────────────────
|
||||||
|
|
@ -202,7 +202,7 @@ describe("Consumer", () => {
|
||||||
const handler = vi.fn().mockImplementation(async () => {
|
const handler = vi.fn().mockImplementation(async () => {
|
||||||
handlerCalls++;
|
handlerCalls++;
|
||||||
if (handlerCalls === 1) {
|
if (handlerCalls === 1) {
|
||||||
throw new TooManyRequestsError("rate limited");
|
throw tooManyRequestsError("rate limited");
|
||||||
}
|
}
|
||||||
// Second call succeeds
|
// Second call succeeds
|
||||||
});
|
});
|
||||||
|
|
|
||||||
266
ts/packages/base/src/__tests__/embeddings-service.test.ts
Normal file
266
ts/packages/base/src/__tests__/embeddings-service.test.ts
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
import { describe, expect, it } from "@effect/vitest";
|
||||||
|
import { ConfigProvider, Effect, Fiber } from "effect";
|
||||||
|
import {
|
||||||
|
Embeddings,
|
||||||
|
EmbeddingsService,
|
||||||
|
MessagingRuntimeLive,
|
||||||
|
PubSub,
|
||||||
|
embeddingsError,
|
||||||
|
runProcessorScoped,
|
||||||
|
topics,
|
||||||
|
type BackendConsumer,
|
||||||
|
type BackendProducer,
|
||||||
|
type CreateConsumerOptions,
|
||||||
|
type CreateProducerOptions,
|
||||||
|
type EmbeddingsRequest,
|
||||||
|
type EmbeddingsResponse,
|
||||||
|
type Message,
|
||||||
|
type PubSubBackend,
|
||||||
|
} from "../index.js";
|
||||||
|
|
||||||
|
function createMessage<T>(value: T, properties: Record<string, string> = {}): Message<T> {
|
||||||
|
return {
|
||||||
|
value: () => value,
|
||||||
|
properties: () => properties,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitFor = (condition: () => boolean, label: string) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
const deadline = Date.now() + 1000;
|
||||||
|
const check = () => {
|
||||||
|
if (condition()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Date.now() > deadline) {
|
||||||
|
reject(new Error(`Timed out waiting for ${label}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(check, 5);
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
}),
|
||||||
|
catch: (error) => error,
|
||||||
|
});
|
||||||
|
|
||||||
|
class RecordingProducer<T> implements BackendProducer<T> {
|
||||||
|
readonly sent: Array<{ readonly message: T; readonly properties?: Record<string, string> }> = [];
|
||||||
|
|
||||||
|
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||||
|
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||||
|
}
|
||||||
|
|
||||||
|
async flush(): Promise<void> {}
|
||||||
|
|
||||||
|
async close(): Promise<void> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PushConsumer<T> implements BackendConsumer<T> {
|
||||||
|
readonly acknowledged: Array<Message<T>> = [];
|
||||||
|
readonly nacked: Array<Message<T>> = [];
|
||||||
|
private readonly messages: Array<Message<T>> = [];
|
||||||
|
private readonly waiters: Array<(message: Message<T> | null) => void> = [];
|
||||||
|
private closed = false;
|
||||||
|
|
||||||
|
push(message: Message<T>): void {
|
||||||
|
const waiter = this.waiters.shift();
|
||||||
|
if (waiter !== undefined) {
|
||||||
|
waiter(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async receive(): Promise<Message<T> | null> {
|
||||||
|
const message = this.messages.shift();
|
||||||
|
if (message !== undefined || this.closed) {
|
||||||
|
return message ?? null;
|
||||||
|
}
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
this.waiters.push(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async acknowledge(message: Message<T>): Promise<void> {
|
||||||
|
this.acknowledged.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async negativeAcknowledge(message: Message<T>): Promise<void> {
|
||||||
|
this.nacked.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribe(): Promise<void> {}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closed = true;
|
||||||
|
for (const waiter of this.waiters.splice(0)) {
|
||||||
|
waiter(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmbeddingsBackend implements PubSubBackend {
|
||||||
|
readonly configConsumer = new PushConsumer<{ readonly version: number; readonly config: Record<string, unknown> }>();
|
||||||
|
readonly consumersByTopic = new Map<string, PushConsumer<unknown>>();
|
||||||
|
readonly producersByTopic = new Map<string, RecordingProducer<unknown>>();
|
||||||
|
closeCount = 0;
|
||||||
|
|
||||||
|
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||||
|
const producer = new RecordingProducer<unknown>();
|
||||||
|
this.producersByTopic.set(options.topic, producer);
|
||||||
|
return producer as BackendProducer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||||
|
if (options.topic === topics.configPush) {
|
||||||
|
return this.configConsumer as unknown as BackendConsumer<T>;
|
||||||
|
}
|
||||||
|
const consumer = new PushConsumer<unknown>();
|
||||||
|
this.consumersByTopic.set(options.topic, consumer);
|
||||||
|
return consumer as BackendConsumer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushConfig(): void {
|
||||||
|
this.configConsumer.push(
|
||||||
|
createMessage({
|
||||||
|
version: 1,
|
||||||
|
config: {
|
||||||
|
flows: {
|
||||||
|
default: {
|
||||||
|
topics: {
|
||||||
|
"embeddings-request": "embeddings-request-topic",
|
||||||
|
"embeddings-response": "embeddings-response-topic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fastMessagingConfig = ConfigProvider.layer(
|
||||||
|
ConfigProvider.fromEnv({
|
||||||
|
TG_CONSUMER_RECEIVE_TIMEOUT_MS: "1",
|
||||||
|
TG_CONSUMER_ERROR_BACKOFF_MS: "1",
|
||||||
|
TG_RATE_LIMIT_RETRY_MS: "1",
|
||||||
|
TG_REQUEST_TIMEOUT_MS: "250",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("EmbeddingsService", () => {
|
||||||
|
it.effect(
|
||||||
|
"handles embeddings requests through the Embeddings Context service",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new EmbeddingsBackend();
|
||||||
|
const embeddingCalls: Array<{ readonly texts: ReadonlyArray<string>; readonly model?: string }> = [];
|
||||||
|
const embeddings = Embeddings.of({
|
||||||
|
embed: Effect.fn("TestEmbeddings.embed")((texts: ReadonlyArray<string>, model?: string) => {
|
||||||
|
embeddingCalls.push(model === undefined ? { texts } : { texts, model });
|
||||||
|
return Effect.succeed(texts.map((text, index) => [text.length, model?.length ?? 0, index]));
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fiber = yield* runService(backend, embeddings).pipe(Effect.forkChild);
|
||||||
|
|
||||||
|
backend.pushConfig();
|
||||||
|
yield* waitFor(() => backend.consumersByTopic.has("embeddings-request-topic"), "embeddings consumer");
|
||||||
|
yield* waitFor(() => backend.producersByTopic.has("embeddings-response-topic"), "embeddings producer");
|
||||||
|
|
||||||
|
const input = backend.consumersByTopic.get("embeddings-request-topic") as PushConsumer<EmbeddingsRequest>;
|
||||||
|
const output = backend.producersByTopic.get("embeddings-response-topic") as RecordingProducer<EmbeddingsResponse>;
|
||||||
|
|
||||||
|
input.push(createMessage({ text: ["alpha", "beta"], model: "model-a" }, { id: "request-1" }));
|
||||||
|
yield* waitFor(() => output.sent.length === 1, "embeddings response");
|
||||||
|
|
||||||
|
expect(embeddingCalls).toEqual([{ texts: ["alpha", "beta"], model: "model-a" }]);
|
||||||
|
expect(output.sent).toEqual([
|
||||||
|
{
|
||||||
|
message: { vectors: [[5, 7, 0], [4, 7, 1]] },
|
||||||
|
properties: { id: "request-1" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(input.acknowledged.length).toBe(1);
|
||||||
|
expect(input.nacked).toEqual([]);
|
||||||
|
|
||||||
|
yield* Fiber.interrupt(fiber);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(backend.closeCount).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"returns a wire error response when the Embeddings service fails",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new EmbeddingsBackend();
|
||||||
|
const embeddings = Embeddings.of({
|
||||||
|
embed: Effect.fn("FailingEmbeddings.embed")(() =>
|
||||||
|
Effect.fail(embeddingsError("test.embed", new Error("provider unavailable"), "test")),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fiber = yield* runService(backend, embeddings).pipe(Effect.forkChild);
|
||||||
|
|
||||||
|
backend.pushConfig();
|
||||||
|
yield* waitFor(() => backend.consumersByTopic.has("embeddings-request-topic"), "embeddings consumer");
|
||||||
|
yield* waitFor(() => backend.producersByTopic.has("embeddings-response-topic"), "embeddings producer");
|
||||||
|
|
||||||
|
const input = backend.consumersByTopic.get("embeddings-request-topic") as PushConsumer<EmbeddingsRequest>;
|
||||||
|
const output = backend.producersByTopic.get("embeddings-response-topic") as RecordingProducer<EmbeddingsResponse>;
|
||||||
|
|
||||||
|
input.push(createMessage({ text: ["alpha"] }, { id: "request-1" }));
|
||||||
|
yield* waitFor(() => output.sent.length === 1, "embeddings error response");
|
||||||
|
|
||||||
|
expect(output.sent).toEqual([
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
vectors: [],
|
||||||
|
error: {
|
||||||
|
type: "embeddings-error",
|
||||||
|
message: "provider unavailable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: { id: "request-1" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(input.acknowledged.length).toBe(1);
|
||||||
|
expect(input.nacked).toEqual([]);
|
||||||
|
|
||||||
|
yield* Fiber.interrupt(fiber);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const runService = (
|
||||||
|
backend: EmbeddingsBackend,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) =>
|
||||||
|
runProcessorScoped(
|
||||||
|
{
|
||||||
|
id: "embeddings",
|
||||||
|
pubsubUrl: "nats://unused:4222",
|
||||||
|
metricsPort: 8000,
|
||||||
|
manageProcessSignals: true,
|
||||||
|
},
|
||||||
|
(config) => new EmbeddingsService(config),
|
||||||
|
).pipe(
|
||||||
|
Effect.provideService(Embeddings, embeddings),
|
||||||
|
Effect.provide(MessagingRuntimeLive),
|
||||||
|
Effect.provide(PubSub.layer(backend)),
|
||||||
|
Effect.provide(fastMessagingConfig),
|
||||||
|
);
|
||||||
215
ts/packages/base/src/__tests__/flow-processor-runtime.test.ts
Normal file
215
ts/packages/base/src/__tests__/flow-processor-runtime.test.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
import { describe, expect, it } from "@effect/vitest";
|
||||||
|
import { ConfigProvider, Effect, Fiber } from "effect";
|
||||||
|
import {
|
||||||
|
FlowProcessor,
|
||||||
|
MessagingRuntimeLive,
|
||||||
|
ProducerSpec,
|
||||||
|
PubSub,
|
||||||
|
runProcessorScoped,
|
||||||
|
topics,
|
||||||
|
type BackendConsumer,
|
||||||
|
type BackendProducer,
|
||||||
|
type CreateConsumerOptions,
|
||||||
|
type CreateProducerOptions,
|
||||||
|
type Message,
|
||||||
|
type ProcessorConfig,
|
||||||
|
type PubSubBackend,
|
||||||
|
} from "../index.js";
|
||||||
|
|
||||||
|
function createMessage<T>(value: T, properties: Record<string, string> = {}): Message<T> {
|
||||||
|
return {
|
||||||
|
value: () => value,
|
||||||
|
properties: () => properties,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitFor = (condition: () => boolean, label: string) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
const deadline = Date.now() + 1000;
|
||||||
|
const check = () => {
|
||||||
|
if (condition()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Date.now() > deadline) {
|
||||||
|
reject(new Error(`Timed out waiting for ${label}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(check, 5);
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
}),
|
||||||
|
catch: (error) => error,
|
||||||
|
});
|
||||||
|
|
||||||
|
class RecordingProducer<T> implements BackendProducer<T> {
|
||||||
|
readonly sent: Array<{ readonly message: T; readonly properties?: Record<string, string> }> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
flushCount = 0;
|
||||||
|
|
||||||
|
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||||
|
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||||
|
}
|
||||||
|
|
||||||
|
async flush(): Promise<void> {
|
||||||
|
this.flushCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PushConsumer<T> implements BackendConsumer<T> {
|
||||||
|
readonly acknowledged: Array<Message<T>> = [];
|
||||||
|
readonly nacked: Array<Message<T>> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
private readonly messages: Array<Message<T>> = [];
|
||||||
|
private readonly waiters: Array<(message: Message<T> | null) => void> = [];
|
||||||
|
private closed = false;
|
||||||
|
|
||||||
|
push(message: Message<T>): void {
|
||||||
|
const waiter = this.waiters.shift();
|
||||||
|
if (waiter !== undefined) {
|
||||||
|
waiter(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async receive(): Promise<Message<T> | null> {
|
||||||
|
const message = this.messages.shift();
|
||||||
|
if (message !== undefined || this.closed) {
|
||||||
|
return message ?? null;
|
||||||
|
}
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
this.waiters.push(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async acknowledge(message: Message<T>): Promise<void> {
|
||||||
|
this.acknowledged.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async negativeAcknowledge(message: Message<T>): Promise<void> {
|
||||||
|
this.nacked.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribe(): Promise<void> {}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closed = true;
|
||||||
|
for (const waiter of this.waiters.splice(0)) {
|
||||||
|
waiter(null);
|
||||||
|
}
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FlowProcessorBackend implements PubSubBackend {
|
||||||
|
readonly configConsumer = new PushConsumer<{ readonly version: number; readonly config: Record<string, unknown> }>();
|
||||||
|
readonly producerOptions: Array<CreateProducerOptions> = [];
|
||||||
|
readonly consumerOptions: Array<CreateConsumerOptions> = [];
|
||||||
|
readonly producers: Array<RecordingProducer<unknown>> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
|
||||||
|
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||||
|
this.producerOptions.push(options);
|
||||||
|
const producer = new RecordingProducer<unknown>();
|
||||||
|
this.producers.push(producer);
|
||||||
|
return producer as BackendProducer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||||
|
this.consumerOptions.push(options);
|
||||||
|
if (options.topic === topics.configPush) {
|
||||||
|
return this.configConsumer as unknown as BackendConsumer<T>;
|
||||||
|
}
|
||||||
|
return new PushConsumer<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushConfig(version: number, flows: Record<string, unknown>): void {
|
||||||
|
this.configConsumer.push(createMessage({ version, config: { flows } }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestFlowProcessor extends FlowProcessor {
|
||||||
|
constructor(
|
||||||
|
config: ProcessorConfig,
|
||||||
|
private readonly events: Array<string>,
|
||||||
|
) {
|
||||||
|
super(config);
|
||||||
|
this.registerSpecification(new ProducerSpec<string>("output"));
|
||||||
|
this.registerConfigHandler(async (_config, version) => {
|
||||||
|
this.events.push(`handler:${version}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fastMessagingConfig = ConfigProvider.layer(
|
||||||
|
ConfigProvider.fromEnv({
|
||||||
|
TG_CONSUMER_RECEIVE_TIMEOUT_MS: "1",
|
||||||
|
TG_CONSUMER_ERROR_BACKOFF_MS: "1",
|
||||||
|
TG_RATE_LIMIT_RETRY_MS: "1",
|
||||||
|
TG_REQUEST_TIMEOUT_MS: "250",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("Effect-native FlowProcessor runtime", () => {
|
||||||
|
it.effect(
|
||||||
|
"starts, restarts, and removes flow scopes from config pushes",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new FlowProcessorBackend();
|
||||||
|
const events: Array<string> = [];
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fiber = yield* runProcessorScoped(
|
||||||
|
{
|
||||||
|
id: "flow-processor-test",
|
||||||
|
pubsubUrl: "nats://unused:4222",
|
||||||
|
metricsPort: 8000,
|
||||||
|
manageProcessSignals: true,
|
||||||
|
},
|
||||||
|
(config) => new TestFlowProcessor(config, events),
|
||||||
|
).pipe(
|
||||||
|
Effect.provide(MessagingRuntimeLive),
|
||||||
|
Effect.provide(PubSub.layer(backend)),
|
||||||
|
Effect.provide(fastMessagingConfig),
|
||||||
|
Effect.forkChild,
|
||||||
|
);
|
||||||
|
|
||||||
|
yield* waitFor(() => backend.consumerOptions.length === 1, "config subscription");
|
||||||
|
|
||||||
|
backend.pushConfig(1, { default: { topics: { output: "topic-a" } } });
|
||||||
|
yield* waitFor(() => backend.producers.length === 1, "first flow producer");
|
||||||
|
yield* waitFor(() => backend.configConsumer.acknowledged.length === 1, "first config ack");
|
||||||
|
|
||||||
|
backend.pushConfig(2, { default: { topics: { output: "topic-a" } } });
|
||||||
|
yield* waitFor(() => backend.configConsumer.acknowledged.length === 2, "unchanged config ack");
|
||||||
|
expect(backend.producers.length).toBe(1);
|
||||||
|
|
||||||
|
backend.pushConfig(3, { default: { topics: { output: "topic-b" } } });
|
||||||
|
yield* waitFor(() => backend.producers.length === 2, "restarted flow producer");
|
||||||
|
yield* waitFor(() => backend.producers[0]?.closeCount === 1, "old flow close");
|
||||||
|
|
||||||
|
backend.pushConfig(4, {});
|
||||||
|
yield* waitFor(() => backend.producers[1]?.closeCount === 1, "removed flow close");
|
||||||
|
|
||||||
|
yield* Fiber.interrupt(fiber);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(backend.producerOptions.map((options) => options.topic)).toEqual(["topic-a", "topic-b"]);
|
||||||
|
expect(events).toEqual(["handler:1", "handler:2", "handler:3", "handler:4"]);
|
||||||
|
expect(backend.configConsumer.closeCount).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(backend.closeCount).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
298
ts/packages/base/src/__tests__/flow-spec-runtime.test.ts
Normal file
298
ts/packages/base/src/__tests__/flow-spec-runtime.test.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
import { describe, expect, it } from "@effect/vitest";
|
||||||
|
import { ConfigProvider, Duration, Effect, Fiber } from "effect";
|
||||||
|
import * as TestClock from "effect/testing/TestClock";
|
||||||
|
import {
|
||||||
|
ConsumerSpec,
|
||||||
|
Flow,
|
||||||
|
MessagingRuntimeLive,
|
||||||
|
ParameterSpec,
|
||||||
|
ProducerSpec,
|
||||||
|
PubSub,
|
||||||
|
RequestResponseSpec,
|
||||||
|
type BackendConsumer,
|
||||||
|
type BackendProducer,
|
||||||
|
type CreateConsumerOptions,
|
||||||
|
type CreateProducerOptions,
|
||||||
|
type FlowContext,
|
||||||
|
type Message,
|
||||||
|
type PubSubBackend,
|
||||||
|
} from "../index.js";
|
||||||
|
|
||||||
|
function createMessage<T>(value: T, properties: Record<string, string> = {}): Message<T> {
|
||||||
|
return {
|
||||||
|
value: () => value,
|
||||||
|
properties: () => properties,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecordingProducer<T> implements BackendProducer<T> {
|
||||||
|
readonly sent: Array<{ readonly message: T; readonly properties?: Record<string, string> }> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
flushCount = 0;
|
||||||
|
|
||||||
|
constructor(private readonly onSend?: (message: T, properties?: Record<string, string>) => void) {}
|
||||||
|
|
||||||
|
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||||
|
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||||
|
this.onSend?.(message, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
async flush(): Promise<void> {
|
||||||
|
this.flushCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScriptedConsumer<T> implements BackendConsumer<T> {
|
||||||
|
readonly acknowledged: Array<Message<T>> = [];
|
||||||
|
readonly nacked: Array<Message<T>> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
private readonly messages: Array<Message<T>>;
|
||||||
|
private readonly waiters: Array<(message: Message<T> | null) => void> = [];
|
||||||
|
private closed = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
messages: Array<Message<T>> = [],
|
||||||
|
private readonly waitForMessages = false,
|
||||||
|
) {
|
||||||
|
this.messages = messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(message: Message<T>): void {
|
||||||
|
const waiter = this.waiters.shift();
|
||||||
|
if (waiter !== undefined) {
|
||||||
|
waiter(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async receive(): Promise<Message<T> | null> {
|
||||||
|
const message = this.messages.shift();
|
||||||
|
if (message !== undefined || !this.waitForMessages || this.closed) {
|
||||||
|
return message ?? null;
|
||||||
|
}
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
this.waiters.push(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async acknowledge(message: Message<T>): Promise<void> {
|
||||||
|
this.acknowledged.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async negativeAcknowledge(message: Message<T>): Promise<void> {
|
||||||
|
this.nacked.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribe(): Promise<void> {}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closed = true;
|
||||||
|
for (const waiter of this.waiters.splice(0)) {
|
||||||
|
waiter(null);
|
||||||
|
}
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RuntimeBackend implements PubSubBackend {
|
||||||
|
closeCount = 0;
|
||||||
|
producerOptions: CreateProducerOptions | null = null;
|
||||||
|
consumerOptions: CreateConsumerOptions | null = null;
|
||||||
|
readonly producer: RecordingProducer<unknown>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly consumer: BackendConsumer<unknown>,
|
||||||
|
onSend?: (message: unknown, properties?: Record<string, string>) => void,
|
||||||
|
) {
|
||||||
|
this.producer = new RecordingProducer<unknown>(onSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||||
|
this.producerOptions = options;
|
||||||
|
return this.producer as BackendProducer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||||
|
this.consumerOptions = options;
|
||||||
|
return this.consumer as BackendConsumer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fastMessagingConfig = ConfigProvider.layer(
|
||||||
|
ConfigProvider.fromEnv({
|
||||||
|
TG_CONSUMER_RECEIVE_TIMEOUT_MS: "1",
|
||||||
|
TG_CONSUMER_ERROR_BACKOFF_MS: "1",
|
||||||
|
TG_RATE_LIMIT_RETRY_MS: "1",
|
||||||
|
TG_REQUEST_TIMEOUT_MS: "250",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const provideRuntime = <A, E, R>(
|
||||||
|
backend: RuntimeBackend,
|
||||||
|
effect: Effect.Effect<A, E, R>,
|
||||||
|
) =>
|
||||||
|
effect.pipe(
|
||||||
|
Effect.provide(MessagingRuntimeLive),
|
||||||
|
Effect.provideService(PubSub, PubSub.fromBackend(backend)),
|
||||||
|
Effect.provide(fastMessagingConfig),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("Effect-native flow specifications", () => {
|
||||||
|
it.effect(
|
||||||
|
"starts producer specs through Effect factories and exposes typed accessors",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new RuntimeBackend(new ScriptedConsumer<unknown>());
|
||||||
|
const flow = new Flow(
|
||||||
|
"default",
|
||||||
|
"processor",
|
||||||
|
backend,
|
||||||
|
{ topics: { output: "actual-output" } },
|
||||||
|
[new ProducerSpec<string>("output")],
|
||||||
|
);
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
provideRuntime(
|
||||||
|
backend,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* flow.startEffect();
|
||||||
|
const producer = yield* flow.producerEffect<string>("output");
|
||||||
|
yield* producer.send("request-1", "hello");
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(backend.producerOptions).toEqual({ topic: "actual-output" });
|
||||||
|
expect(backend.producer.sent).toEqual([
|
||||||
|
{ message: "hello", properties: { id: "request-1" } },
|
||||||
|
]);
|
||||||
|
expect(backend.producer.closeCount).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"runs Promise handlers through the explicit ConsumerSpec compatibility helper",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const message = createMessage("payload", { id: "request-1" });
|
||||||
|
const consumer = new ScriptedConsumer<string>([message]);
|
||||||
|
const backend = new RuntimeBackend(consumer as BackendConsumer<unknown>);
|
||||||
|
const handled: Array<string> = [];
|
||||||
|
const flow = new Flow(
|
||||||
|
"default",
|
||||||
|
"processor",
|
||||||
|
backend,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
ConsumerSpec.fromPromise<string>(
|
||||||
|
"input",
|
||||||
|
async (value, properties, flowContext: FlowContext) => {
|
||||||
|
handled.push(`${flowContext.name}:${properties.id}:${value}`);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
provideRuntime(
|
||||||
|
backend,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* flow.startEffect();
|
||||||
|
yield* Effect.yieldNow;
|
||||||
|
yield* TestClock.adjust(Duration.millis(5));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(consumer.acknowledged).toEqual([message]);
|
||||||
|
expect(consumer.nacked).toEqual([]);
|
||||||
|
expect(handled).toEqual(["default:request-1:payload"]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"registers request-response specs through Effect queues and keeps the Promise facade working",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const responseConsumer = new ScriptedConsumer<string>([], true);
|
||||||
|
const backend = new RuntimeBackend(
|
||||||
|
responseConsumer as BackendConsumer<unknown>,
|
||||||
|
(_message, properties) => {
|
||||||
|
responseConsumer.push(createMessage("response", { id: properties?.id ?? "" }));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const flow = new Flow(
|
||||||
|
"default",
|
||||||
|
"processor",
|
||||||
|
backend,
|
||||||
|
{
|
||||||
|
topics: {
|
||||||
|
request: "actual-request",
|
||||||
|
response: "actual-response",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[new RequestResponseSpec<string, string>("rr", "request", "response")],
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = yield* Effect.scoped(
|
||||||
|
provideRuntime(
|
||||||
|
backend,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* flow.startEffect();
|
||||||
|
const requestor = flow.requestor<string, string>("rr");
|
||||||
|
const fiber = yield* Effect.promise(() =>
|
||||||
|
requestor.request("request", { timeoutMs: 250 }),
|
||||||
|
).pipe(Effect.forkChild);
|
||||||
|
yield* TestClock.adjust(Duration.millis(5));
|
||||||
|
return yield* Fiber.join(fiber);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toBe("response");
|
||||||
|
expect(backend.producerOptions).toEqual({ topic: "actual-request" });
|
||||||
|
expect(responseConsumer.acknowledged.length).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"returns typed errors for missing flow resources",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new RuntimeBackend(new ScriptedConsumer<unknown>());
|
||||||
|
const flow = new Flow(
|
||||||
|
"default",
|
||||||
|
"processor",
|
||||||
|
backend,
|
||||||
|
{ parameters: { present: 42 } },
|
||||||
|
[new ParameterSpec("present")],
|
||||||
|
);
|
||||||
|
|
||||||
|
const errors = yield* Effect.scoped(
|
||||||
|
provideRuntime(
|
||||||
|
backend,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* flow.startEffect();
|
||||||
|
const producerError = yield* flow.producerEffect<string>("missing-producer").pipe(Effect.flip);
|
||||||
|
const parameter = yield* flow.parameterEffect<number>("present");
|
||||||
|
const parameterError = yield* flow.parameterEffect<number>("missing-parameter").pipe(Effect.flip);
|
||||||
|
return { producerError, parameter, parameterError };
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(errors.parameter).toBe(42);
|
||||||
|
expect(errors.producerError._tag).toBe("FlowResourceNotFoundError");
|
||||||
|
expect(errors.producerError.resourceType).toBe("producer");
|
||||||
|
expect(errors.producerError.resourceName).toBe("missing-producer");
|
||||||
|
expect(errors.parameterError._tag).toBe("FlowResourceNotFoundError");
|
||||||
|
expect(errors.parameterError.resourceType).toBe("parameter");
|
||||||
|
expect(() => flow.producer("missing-producer")).toThrow("not found");
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
277
ts/packages/base/src/__tests__/messaging-runtime.test.ts
Normal file
277
ts/packages/base/src/__tests__/messaging-runtime.test.ts
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
import { describe, expect, it } from "@effect/vitest";
|
||||||
|
import { Duration, Effect, Fiber } from "effect";
|
||||||
|
import * as TestClock from "effect/testing/TestClock";
|
||||||
|
import {
|
||||||
|
PubSub,
|
||||||
|
defaultMessagingRuntimeConfig,
|
||||||
|
makeEffectRequestResponseFromPubSub,
|
||||||
|
MessagingRuntimeLive,
|
||||||
|
ProducerSpec,
|
||||||
|
runEffectConsumerScoped,
|
||||||
|
runEffectProducerScoped,
|
||||||
|
runFlowScoped,
|
||||||
|
type BackendConsumer,
|
||||||
|
type BackendProducer,
|
||||||
|
type CreateConsumerOptions,
|
||||||
|
type CreateProducerOptions,
|
||||||
|
type FlowContext,
|
||||||
|
type Message,
|
||||||
|
type PubSubBackend,
|
||||||
|
} from "../index.js";
|
||||||
|
import type { Flow } from "../processor/flow.js";
|
||||||
|
import { Flow as RuntimeFlow } from "../processor/flow.js";
|
||||||
|
|
||||||
|
function createMessage<T>(value: T, properties: Record<string, string> = {}): Message<T> {
|
||||||
|
return {
|
||||||
|
value: () => value,
|
||||||
|
properties: () => properties,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecordingProducer<T> implements BackendProducer<T> {
|
||||||
|
readonly sent: Array<{ readonly message: T; readonly properties?: Record<string, string> }> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
flushCount = 0;
|
||||||
|
|
||||||
|
constructor(private readonly onSend?: (message: T, properties?: Record<string, string>) => void) {}
|
||||||
|
|
||||||
|
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||||
|
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||||
|
this.onSend?.(message, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
async flush(): Promise<void> {
|
||||||
|
this.flushCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScriptedConsumer<T> implements BackendConsumer<T> {
|
||||||
|
readonly acknowledged: Array<Message<T>> = [];
|
||||||
|
readonly nacked: Array<Message<T>> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
private readonly messages: Array<Message<T>>;
|
||||||
|
|
||||||
|
constructor(messages: Array<Message<T>> = []) {
|
||||||
|
this.messages = messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(message: Message<T>): void {
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async receive(): Promise<Message<T> | null> {
|
||||||
|
const message = this.messages.shift();
|
||||||
|
if (message !== undefined) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async acknowledge(message: Message<T>): Promise<void> {
|
||||||
|
this.acknowledged.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async negativeAcknowledge(message: Message<T>): Promise<void> {
|
||||||
|
this.nacked.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribe(): Promise<void> {}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RuntimeBackend implements PubSubBackend {
|
||||||
|
closeCount = 0;
|
||||||
|
producerOptions: CreateProducerOptions | null = null;
|
||||||
|
consumerOptions: CreateConsumerOptions | null = null;
|
||||||
|
readonly producer: RecordingProducer<unknown>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly consumer: BackendConsumer<unknown>,
|
||||||
|
onSend?: (message: unknown, properties?: Record<string, string>) => void,
|
||||||
|
) {
|
||||||
|
this.producer = new RecordingProducer<unknown>(onSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||||
|
this.producerOptions = options;
|
||||||
|
return this.producer as BackendProducer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||||
|
this.consumerOptions = options;
|
||||||
|
return this.consumer as BackendConsumer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flowContext: FlowContext = {
|
||||||
|
id: "processor",
|
||||||
|
name: "default",
|
||||||
|
flow: {} as Flow,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Effect-native messaging runtime", () => {
|
||||||
|
it.effect(
|
||||||
|
"creates scoped producers through PubSub and translates send calls",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const consumer = new ScriptedConsumer<unknown>();
|
||||||
|
const backend = new RuntimeBackend(consumer);
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const producer = yield* runEffectProducerScoped<string>({ topic: "tg.test.producer" });
|
||||||
|
yield* producer.send("message-1", "hello");
|
||||||
|
|
||||||
|
expect(backend.producerOptions).toEqual({ topic: "tg.test.producer" });
|
||||||
|
expect(backend.producer.sent).toEqual([
|
||||||
|
{ message: "hello", properties: { id: "message-1" } },
|
||||||
|
]);
|
||||||
|
}).pipe(Effect.provide(PubSub.layer(backend))),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(backend.producer.closeCount).toBe(1);
|
||||||
|
expect(backend.closeCount).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"runs consumers as scoped fibers and acknowledges handled messages",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const message = createMessage("payload", { id: "request-1" });
|
||||||
|
const consumer = new ScriptedConsumer<string>([message]);
|
||||||
|
const backend = new RuntimeBackend(consumer as BackendConsumer<unknown>);
|
||||||
|
const handled: Array<string> = [];
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* runEffectConsumerScoped<string>(
|
||||||
|
{
|
||||||
|
topic: "tg.test.consumer",
|
||||||
|
subscription: "sub",
|
||||||
|
receiveTimeoutMs: 1,
|
||||||
|
errorBackoffMs: 1,
|
||||||
|
handler: (value, properties) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
handled.push(`${properties.id}:${value}`);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
flowContext,
|
||||||
|
);
|
||||||
|
yield* TestClock.adjust(Duration.millis(20));
|
||||||
|
}).pipe(Effect.provide(PubSub.layer(backend))),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(handled).toEqual(["request-1:payload"]);
|
||||||
|
expect(consumer.acknowledged).toEqual([message]);
|
||||||
|
expect(consumer.nacked).toEqual([]);
|
||||||
|
expect(consumer.closeCount).toBeGreaterThan(0);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"routes request-response replies through an Effect queue",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const responseConsumer = new ScriptedConsumer<string>();
|
||||||
|
const backend = new RuntimeBackend(
|
||||||
|
responseConsumer as BackendConsumer<unknown>,
|
||||||
|
(_message, properties) => {
|
||||||
|
responseConsumer.push(createMessage("response", { id: properties?.id ?? "" }));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const requestor = yield* makeEffectRequestResponseFromPubSub<string, string>(
|
||||||
|
PubSub.fromBackend(backend),
|
||||||
|
{
|
||||||
|
...defaultMessagingRuntimeConfig,
|
||||||
|
consumerReceiveTimeoutMs: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requestTopic: "tg.test.request",
|
||||||
|
responseTopic: "tg.test.response",
|
||||||
|
subscription: "sub",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const fiber = yield* requestor.request("request", { timeoutMs: 250 }).pipe(Effect.forkChild);
|
||||||
|
yield* TestClock.adjust(Duration.millis(5));
|
||||||
|
return yield* Fiber.join(fiber);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toBe("response");
|
||||||
|
expect(backend.producer.sent[0]?.message).toBe("request");
|
||||||
|
expect(responseConsumer.acknowledged.length).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"fails request-response calls with a typed timeout",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const responseConsumer = new ScriptedConsumer<string>();
|
||||||
|
const backend = new RuntimeBackend(responseConsumer as BackendConsumer<unknown>);
|
||||||
|
|
||||||
|
const error = yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const requestor = yield* makeEffectRequestResponseFromPubSub<string, string>(
|
||||||
|
PubSub.fromBackend(backend),
|
||||||
|
{
|
||||||
|
...defaultMessagingRuntimeConfig,
|
||||||
|
consumerReceiveTimeoutMs: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requestTopic: "tg.test.request",
|
||||||
|
responseTopic: "tg.test.response",
|
||||||
|
subscription: "sub",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const fiber = yield* requestor.request("request", { timeoutMs: 5 }).pipe(
|
||||||
|
Effect.flip,
|
||||||
|
Effect.forkChild,
|
||||||
|
);
|
||||||
|
yield* TestClock.adjust(Duration.millis(10));
|
||||||
|
return yield* Fiber.join(fiber);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(error._tag).toBe("MessagingTimeoutError");
|
||||||
|
expect(error.operation).toBe("request-response");
|
||||||
|
expect(error.timeoutMs).toBe(5);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"owns Flow lifecycle through a scoped Effect boundary",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const consumer = new ScriptedConsumer<unknown>();
|
||||||
|
const backend = new RuntimeBackend(consumer);
|
||||||
|
const flow = new RuntimeFlow(
|
||||||
|
"flow-a",
|
||||||
|
"processor",
|
||||||
|
backend,
|
||||||
|
{},
|
||||||
|
[new ProducerSpec<string>("flow-output")],
|
||||||
|
);
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
runFlowScoped(flow).pipe(
|
||||||
|
Effect.provide(MessagingRuntimeLive),
|
||||||
|
Effect.provideService(PubSub, PubSub.fromBackend(backend)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(backend.producerOptions).toEqual({ topic: "flow-output" });
|
||||||
|
expect(backend.producer.closeCount).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
240
ts/packages/base/src/__tests__/runtime-services.test.ts
Normal file
240
ts/packages/base/src/__tests__/runtime-services.test.ts
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
import { describe, expect, it } from "@effect/vitest";
|
||||||
|
import { Effect } from "effect";
|
||||||
|
import {
|
||||||
|
AsyncProcessor,
|
||||||
|
PubSub,
|
||||||
|
runProcessorScoped,
|
||||||
|
type BackendConsumer,
|
||||||
|
type BackendProducer,
|
||||||
|
type CreateConsumerOptions,
|
||||||
|
type CreateProducerOptions,
|
||||||
|
type Message,
|
||||||
|
type ProcessorConfig,
|
||||||
|
type PubSubBackend,
|
||||||
|
} from "../index.js";
|
||||||
|
|
||||||
|
class FakeProducer<T> implements BackendProducer<T> {
|
||||||
|
readonly sent: Array<{ readonly message: T; readonly properties?: Record<string, string> }> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
flushCount = 0;
|
||||||
|
|
||||||
|
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||||
|
this.sent.push(
|
||||||
|
properties === undefined
|
||||||
|
? { message }
|
||||||
|
: { message, properties },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async flush(): Promise<void> {
|
||||||
|
this.flushCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeConsumer<T> implements BackendConsumer<T> {
|
||||||
|
closeCount = 0;
|
||||||
|
|
||||||
|
async receive(): Promise<Message<T> | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async acknowledge(): Promise<void> {}
|
||||||
|
|
||||||
|
async negativeAcknowledge(): Promise<void> {}
|
||||||
|
|
||||||
|
async unsubscribe(): Promise<void> {}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakePubSubBackend implements PubSubBackend {
|
||||||
|
closeCount = 0;
|
||||||
|
producerOptions: CreateProducerOptions | null = null;
|
||||||
|
consumerOptions: CreateConsumerOptions | null = null;
|
||||||
|
|
||||||
|
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||||
|
this.producerOptions = options;
|
||||||
|
return new FakeProducer<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||||
|
this.consumerOptions = options;
|
||||||
|
return new FakeConsumer<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FailingProducerBackend extends FakePubSubBackend {
|
||||||
|
override async createProducer<T>(): Promise<BackendProducer<T>> {
|
||||||
|
throw new Error("producer unavailable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecordingProcessor extends AsyncProcessor {
|
||||||
|
constructor(
|
||||||
|
config: ProcessorConfig,
|
||||||
|
private readonly events: Array<string>,
|
||||||
|
) {
|
||||||
|
super(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async run(): Promise<void> {
|
||||||
|
this.events.push(`run:${this.config.manageProcessSignals === false ? "effect-signals" : "class-signals"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async stop(): Promise<void> {
|
||||||
|
this.events.push("stop");
|
||||||
|
await super.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FailingProcessor extends AsyncProcessor {
|
||||||
|
protected async run(): Promise<void> {
|
||||||
|
throw new Error("processor failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NativeRecordingProcessor extends AsyncProcessor<never, PubSub> {
|
||||||
|
constructor(
|
||||||
|
config: ProcessorConfig,
|
||||||
|
private readonly events: Array<string>,
|
||||||
|
) {
|
||||||
|
super(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override runEffect() {
|
||||||
|
const events = this.events;
|
||||||
|
const config = this.config;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
events.push(`native:${config.manageProcessSignals === false ? "effect-signals" : "class-signals"}`);
|
||||||
|
events.push(`pubsub:${pubsub.backend.constructor.name}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override stopEffect() {
|
||||||
|
this.events.push("native-stop");
|
||||||
|
return super.stopEffect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Effect runtime services", () => {
|
||||||
|
it.effect(
|
||||||
|
"provides a compatibility backend through the PubSub service",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new FakePubSubBackend();
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
const producer = yield* pubsub.createProducer<string>({ topic: "tg.test.topic" });
|
||||||
|
yield* Effect.promise(() => producer.send("hello", { id: "1" }));
|
||||||
|
|
||||||
|
expect(backend.producerOptions).toEqual({ topic: "tg.test.topic" });
|
||||||
|
expect(pubsub.backend).toBe(backend);
|
||||||
|
}).pipe(Effect.provide(PubSub.layer(backend))),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(backend.closeCount).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"maps backend failures into PubSubError",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new FailingProducerBackend();
|
||||||
|
|
||||||
|
const error = yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
return yield* pubsub.createProducer<string>({ topic: "tg.test.failure" }).pipe(Effect.flip);
|
||||||
|
}).pipe(Effect.provide(PubSub.layer(backend))),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(error._tag).toBe("PubSubError");
|
||||||
|
expect(error.operation).toBe("createProducer:tg.test.failure");
|
||||||
|
expect(error.message).toBe("producer unavailable");
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"runs a processor with injected PubSub and scoped finalization",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new FakePubSubBackend();
|
||||||
|
const events: Array<string> = [];
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
runProcessorScoped(
|
||||||
|
{
|
||||||
|
id: "recording",
|
||||||
|
pubsubUrl: "nats://unused:4222",
|
||||||
|
metricsPort: 8000,
|
||||||
|
manageProcessSignals: true,
|
||||||
|
},
|
||||||
|
(config) => new RecordingProcessor(config, events),
|
||||||
|
).pipe(Effect.provide(PubSub.layer(backend))),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(events).toEqual(["run:effect-signals", "stop"]);
|
||||||
|
expect(backend.closeCount).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"runs native processor lifecycle hooks with Effect requirements",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new FakePubSubBackend();
|
||||||
|
const events: Array<string> = [];
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
runProcessorScoped(
|
||||||
|
{
|
||||||
|
id: "native-recording",
|
||||||
|
pubsubUrl: "nats://unused:4222",
|
||||||
|
metricsPort: 8000,
|
||||||
|
manageProcessSignals: true,
|
||||||
|
},
|
||||||
|
(config) => new NativeRecordingProcessor(config, events),
|
||||||
|
).pipe(Effect.provide(PubSub.layer(backend))),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(events).toEqual(["native:effect-signals", "pubsub:FakePubSubBackend", "native-stop"]);
|
||||||
|
expect(backend.closeCount).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"maps processor start failures into ProcessorLifecycleError",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new FakePubSubBackend();
|
||||||
|
|
||||||
|
const error = yield* Effect.scoped(
|
||||||
|
runProcessorScoped(
|
||||||
|
{
|
||||||
|
id: "failing",
|
||||||
|
metricsPort: 8000,
|
||||||
|
manageProcessSignals: true,
|
||||||
|
},
|
||||||
|
(config) => new FailingProcessor(config),
|
||||||
|
).pipe(
|
||||||
|
Effect.provide(PubSub.layer(backend)),
|
||||||
|
Effect.flip,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(error._tag).toBe("ProcessorLifecycleError");
|
||||||
|
expect(error.operation).toBe("start");
|
||||||
|
expect(error.processorId).toBe("failing");
|
||||||
|
expect(error.message).toBe("processor failed");
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
95
ts/packages/base/src/__tests__/schema-effect.test.ts
Normal file
95
ts/packages/base/src/__tests__/schema-effect.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { describe, expect, it } from "@effect/vitest";
|
||||||
|
import { ConfigProvider, Effect } from "effect";
|
||||||
|
import * as S from "effect/Schema";
|
||||||
|
import {
|
||||||
|
ConfigRequest,
|
||||||
|
GraphRagResponse,
|
||||||
|
Term,
|
||||||
|
TextCompletionRequest,
|
||||||
|
loadProcessorRuntimeConfig,
|
||||||
|
} from "../index.js";
|
||||||
|
|
||||||
|
describe("Effect schemas", () => {
|
||||||
|
it.effect(
|
||||||
|
"decode existing text-completion wire payloads",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const request = yield* S.decodeUnknownEffect(TextCompletionRequest)({
|
||||||
|
system: "system",
|
||||||
|
prompt: "hello",
|
||||||
|
streaming: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request.prompt).toBe("hello");
|
||||||
|
expect(request.streaming).toBe(true);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"decode recursive RDF terms",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const term = yield* S.decodeUnknownEffect(Term)({
|
||||||
|
type: "TRIPLE",
|
||||||
|
triple: {
|
||||||
|
s: { type: "IRI", iri: "urn:s" },
|
||||||
|
p: { type: "IRI", iri: "urn:p" },
|
||||||
|
o: { type: "LITERAL", value: "object" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(term.type).toBe("TRIPLE");
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"preserve gateway response extension fields",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const response = yield* S.decodeUnknownEffect(GraphRagResponse)({
|
||||||
|
response: "ok",
|
||||||
|
message_type: "explain",
|
||||||
|
explain_id: "e1",
|
||||||
|
providerTrace: { kept: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.providerTrace).toEqual({ kept: true });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"decode config requests",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const request = yield* S.decodeUnknownEffect(ConfigRequest)({
|
||||||
|
operation: "put",
|
||||||
|
keys: ["flows"],
|
||||||
|
values: { default: { topics: {} } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request.operation).toBe("put");
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Effect runtime config", () => {
|
||||||
|
it.effect(
|
||||||
|
"loads processor settings from existing env names",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const provider = ConfigProvider.fromEnv({
|
||||||
|
env: {
|
||||||
|
NATS_URL: "nats://example:4222",
|
||||||
|
METRICS_PORT: "9000",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = yield* Effect.provide(
|
||||||
|
loadProcessorRuntimeConfig("svc", { manageProcessSignals: false }),
|
||||||
|
ConfigProvider.layer(provider),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config).toEqual({
|
||||||
|
id: "svc",
|
||||||
|
pubsubUrl: "nats://example:4222",
|
||||||
|
metricsPort: 9000,
|
||||||
|
manageProcessSignals: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -10,3 +10,11 @@ export type {
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
export { NatsBackend } from "./nats.js";
|
export { NatsBackend } from "./nats.js";
|
||||||
|
export {
|
||||||
|
PubSub,
|
||||||
|
NatsPubSubLive,
|
||||||
|
makeNatsPubSubLayer,
|
||||||
|
makePubSubService,
|
||||||
|
pubSubLayer,
|
||||||
|
type PubSubService,
|
||||||
|
} from "./pubsub.js";
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
AckPolicy,
|
AckPolicy,
|
||||||
DeliverPolicy,
|
DeliverPolicy,
|
||||||
} from "nats";
|
} from "nats";
|
||||||
|
import * as S from "effect/Schema";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
PubSubBackend,
|
PubSubBackend,
|
||||||
|
|
@ -34,12 +35,11 @@ const sc = StringCodec();
|
||||||
class NatsMessage<T> implements Message<T> {
|
class NatsMessage<T> implements Message<T> {
|
||||||
/** Exposed so acknowledge/negativeAcknowledge can access the raw JsMsg */
|
/** Exposed so acknowledge/negativeAcknowledge can access the raw JsMsg */
|
||||||
readonly _jsMsg: JsMsg;
|
readonly _jsMsg: JsMsg;
|
||||||
|
private readonly decoded: T;
|
||||||
|
|
||||||
constructor(
|
constructor(msg: JsMsg, decoded: T) {
|
||||||
msg: JsMsg,
|
|
||||||
private readonly decoded: T,
|
|
||||||
) {
|
|
||||||
this._jsMsg = msg;
|
this._jsMsg = msg;
|
||||||
|
this.decoded = decoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
value(): T {
|
value(): T {
|
||||||
|
|
@ -49,9 +49,12 @@ class NatsMessage<T> implements Message<T> {
|
||||||
properties(): Record<string, string> {
|
properties(): Record<string, string> {
|
||||||
const headers = this._jsMsg.headers;
|
const headers = this._jsMsg.headers;
|
||||||
const props: Record<string, string> = {};
|
const props: Record<string, string> = {};
|
||||||
if (headers) {
|
if (headers !== undefined) {
|
||||||
for (const [key, values] of headers) {
|
for (const [key, values] of headers) {
|
||||||
props[key] = values[0];
|
const value = values[0];
|
||||||
|
if (value !== undefined) {
|
||||||
|
props[key] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return props;
|
return props;
|
||||||
|
|
@ -59,16 +62,24 @@ class NatsMessage<T> implements Message<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class NatsProducer<T> implements BackendProducer<T> {
|
class NatsProducer<T> implements BackendProducer<T> {
|
||||||
constructor(
|
private readonly js: JetStreamClient;
|
||||||
private readonly js: JetStreamClient,
|
private readonly subject: string;
|
||||||
private readonly subject: string,
|
private readonly schema: S.Top | undefined;
|
||||||
) {}
|
|
||||||
|
constructor(js: JetStreamClient, subject: string, schema?: S.Top) {
|
||||||
|
this.js = js;
|
||||||
|
this.subject = subject;
|
||||||
|
this.schema = schema;
|
||||||
|
}
|
||||||
|
|
||||||
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||||
const data = sc.encode(JSON.stringify(message));
|
const encoded = this.schema !== undefined
|
||||||
|
? S.encodeUnknownSync(this.schema as S.Codec<unknown, unknown>)(message)
|
||||||
|
: message;
|
||||||
|
const data = sc.encode(JSON.stringify(encoded));
|
||||||
const opts: Record<string, unknown> = {};
|
const opts: Record<string, unknown> = {};
|
||||||
|
|
||||||
if (properties && Object.keys(properties).length > 0) {
|
if (properties !== undefined && Object.keys(properties).length > 0) {
|
||||||
const { headers } = await import("nats");
|
const { headers } = await import("nats");
|
||||||
const hdrs = headers();
|
const hdrs = headers();
|
||||||
for (const [key, val] of Object.entries(properties)) {
|
for (const [key, val] of Object.entries(properties)) {
|
||||||
|
|
@ -91,15 +102,31 @@ class NatsProducer<T> implements BackendProducer<T> {
|
||||||
|
|
||||||
class NatsConsumer<T> implements BackendConsumer<T> {
|
class NatsConsumer<T> implements BackendConsumer<T> {
|
||||||
private consumer: NatsJsConsumer | null = null;
|
private consumer: NatsJsConsumer | null = null;
|
||||||
|
private readonly js: JetStreamClient;
|
||||||
|
private readonly jsm: JetStreamManager;
|
||||||
|
private readonly subject: string;
|
||||||
|
private readonly subscription: string;
|
||||||
|
private readonly initialPosition: "latest" | "earliest";
|
||||||
|
private readonly streamName: string;
|
||||||
|
private readonly schema: S.Top | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly js: JetStreamClient,
|
js: JetStreamClient,
|
||||||
private readonly jsm: JetStreamManager,
|
jsm: JetStreamManager,
|
||||||
private readonly subject: string,
|
subject: string,
|
||||||
private readonly subscription: string,
|
subscription: string,
|
||||||
private readonly initialPosition: "latest" | "earliest",
|
initialPosition: "latest" | "earliest",
|
||||||
private readonly streamName: string,
|
streamName: string,
|
||||||
) {}
|
schema?: S.Top,
|
||||||
|
) {
|
||||||
|
this.js = js;
|
||||||
|
this.jsm = jsm;
|
||||||
|
this.subject = subject;
|
||||||
|
this.subscription = subscription;
|
||||||
|
this.initialPosition = initialPosition;
|
||||||
|
this.streamName = streamName;
|
||||||
|
this.schema = schema;
|
||||||
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
// Stream is already ensured by NatsBackend.ensureStream().
|
// Stream is already ensured by NatsBackend.ensureStream().
|
||||||
|
|
@ -124,14 +151,17 @@ class NatsConsumer<T> implements BackendConsumer<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async receive(timeoutMs = 2000): Promise<Message<T> | null> {
|
async receive(timeoutMs = 2000): Promise<Message<T> | null> {
|
||||||
if (!this.consumer) throw new Error("Consumer not initialized");
|
if (this.consumer === null) throw new Error("Consumer not initialized");
|
||||||
|
|
||||||
// Pull a single message with a timeout using the pull-based API.
|
// Pull a single message with a timeout using the pull-based API.
|
||||||
// consumer.next() returns a JsMsg or null when the timeout expires.
|
// consumer.next() returns a JsMsg or null when the timeout expires.
|
||||||
const msg = await this.consumer.next({ expires: timeoutMs });
|
const msg = await this.consumer.next({ expires: timeoutMs });
|
||||||
if (!msg) return null;
|
if (msg === null) return null;
|
||||||
|
|
||||||
const decoded = JSON.parse(sc.decode(msg.data)) as T;
|
const parsed = JSON.parse(sc.decode(msg.data));
|
||||||
|
const decoded = this.schema !== undefined
|
||||||
|
? S.decodeUnknownSync(this.schema as S.Codec<unknown, unknown>)(parsed) as T
|
||||||
|
: parsed as T;
|
||||||
return new NatsMessage(msg, decoded);
|
return new NatsMessage(msg, decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,11 +191,14 @@ export class NatsBackend implements PubSubBackend {
|
||||||
private js: JetStreamClient | null = null;
|
private js: JetStreamClient | null = null;
|
||||||
private jsm: JetStreamManager | null = null;
|
private jsm: JetStreamManager | null = null;
|
||||||
private initializedStreams = new Set<string>();
|
private initializedStreams = new Set<string>();
|
||||||
|
private readonly url: string;
|
||||||
|
|
||||||
constructor(private readonly url: string = "nats://localhost:4222") {}
|
constructor(url = "nats://localhost:4222") {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
private async ensureConnected(): Promise<void> {
|
private async ensureConnected(): Promise<void> {
|
||||||
if (!this.connection) {
|
if (this.connection === null) {
|
||||||
this.connection = await connect({ servers: this.url });
|
this.connection = await connect({ servers: this.url });
|
||||||
this.js = this.connection.jetstream();
|
this.js = this.connection.jetstream();
|
||||||
this.jsm = await this.connection.jetstreamManager();
|
this.jsm = await this.connection.jetstreamManager();
|
||||||
|
|
@ -184,10 +217,13 @@ export class NatsBackend implements PubSubBackend {
|
||||||
|
|
||||||
const wildcardSubject = `${parts.slice(0, 2).join(".")}.>`;
|
const wildcardSubject = `${parts.slice(0, 2).join(".")}.>`;
|
||||||
|
|
||||||
|
const jsm = this.jsm;
|
||||||
|
if (jsm === null) throw new Error("NATS backend not connected");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.jsm!.streams.info(streamName);
|
await jsm.streams.info(streamName);
|
||||||
} catch {
|
} catch {
|
||||||
await this.jsm!.streams.add({
|
await jsm.streams.add({
|
||||||
name: streamName,
|
name: streamName,
|
||||||
subjects: [wildcardSubject],
|
subjects: [wildcardSubject],
|
||||||
});
|
});
|
||||||
|
|
@ -199,26 +235,32 @@ export class NatsBackend implements PubSubBackend {
|
||||||
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||||
await this.ensureConnected();
|
await this.ensureConnected();
|
||||||
await this.ensureStream(options.topic);
|
await this.ensureStream(options.topic);
|
||||||
return new NatsProducer<T>(this.js!, options.topic);
|
const js = this.js;
|
||||||
|
if (js === null) throw new Error("NATS backend not connected");
|
||||||
|
return new NatsProducer<T>(js, options.topic, options.schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||||
await this.ensureConnected();
|
await this.ensureConnected();
|
||||||
const streamName = await this.ensureStream(options.topic);
|
const streamName = await this.ensureStream(options.topic);
|
||||||
|
const js = this.js;
|
||||||
|
const jsm = this.jsm;
|
||||||
|
if (js === null || jsm === null) throw new Error("NATS backend not connected");
|
||||||
const consumer = new NatsConsumer<T>(
|
const consumer = new NatsConsumer<T>(
|
||||||
this.js!,
|
js,
|
||||||
this.jsm!,
|
jsm,
|
||||||
options.topic,
|
options.topic,
|
||||||
options.subscription,
|
options.subscription,
|
||||||
options.initialPosition ?? "latest",
|
options.initialPosition ?? "latest",
|
||||||
streamName,
|
streamName,
|
||||||
|
options.schema,
|
||||||
);
|
);
|
||||||
await consumer.init();
|
await consumer.init();
|
||||||
return consumer;
|
return consumer;
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
if (this.connection) {
|
if (this.connection !== null) {
|
||||||
await this.connection.drain();
|
await this.connection.drain();
|
||||||
this.connection = null;
|
this.connection = null;
|
||||||
this.js = null;
|
this.js = null;
|
||||||
|
|
|
||||||
101
ts/packages/base/src/backend/pubsub.ts
Normal file
101
ts/packages/base/src/backend/pubsub.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/**
|
||||||
|
* Effect-native pub/sub capability for runtime composition.
|
||||||
|
*
|
||||||
|
* The existing Promise-based backend protocol stays available as the
|
||||||
|
* compatibility bridge while service code moves to `Context.Service`/Layers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Config, Context, Effect, Layer } from "effect";
|
||||||
|
import * as O from "effect/Option";
|
||||||
|
import type {
|
||||||
|
BackendConsumer,
|
||||||
|
BackendProducer,
|
||||||
|
CreateConsumerOptions,
|
||||||
|
CreateProducerOptions,
|
||||||
|
PubSubBackend,
|
||||||
|
} from "./types.js";
|
||||||
|
import { NatsBackend } from "./nats.js";
|
||||||
|
import { pubSubError } from "../errors.js";
|
||||||
|
|
||||||
|
export interface PubSubService {
|
||||||
|
readonly backend: PubSubBackend;
|
||||||
|
readonly createProducer: <T>(
|
||||||
|
options: CreateProducerOptions,
|
||||||
|
) => Effect.Effect<BackendProducer<T>, ReturnType<typeof pubSubError>>;
|
||||||
|
readonly createConsumer: <T>(
|
||||||
|
options: CreateConsumerOptions,
|
||||||
|
) => Effect.Effect<BackendConsumer<T>, ReturnType<typeof pubSubError>>;
|
||||||
|
readonly close: Effect.Effect<void, ReturnType<typeof pubSubError>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PubSub extends Context.Service<PubSub, PubSubService>()("@trustgraph/base/backend/pubsub") {
|
||||||
|
static fromBackend(backend: PubSubBackend): PubSubService {
|
||||||
|
return makePubSubService(backend);
|
||||||
|
}
|
||||||
|
|
||||||
|
static layer(backend: PubSubBackend): Layer.Layer<PubSub> {
|
||||||
|
return pubSubLayer(backend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makePubSubService(backend: PubSubBackend): PubSubService {
|
||||||
|
return {
|
||||||
|
backend,
|
||||||
|
createProducer: <T>(options: CreateProducerOptions) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => backend.createProducer<T>(options),
|
||||||
|
catch: (error) => pubSubError(`createProducer:${options.topic}`, error),
|
||||||
|
}),
|
||||||
|
createConsumer: <T>(options: CreateConsumerOptions) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => backend.createConsumer<T>(options),
|
||||||
|
catch: (error) => pubSubError(`createConsumer:${options.topic}`, error),
|
||||||
|
}),
|
||||||
|
close: Effect.tryPromise({
|
||||||
|
try: () => backend.close(),
|
||||||
|
catch: (error) => pubSubError("close", error),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pubSubLayer(backend: PubSubBackend): Layer.Layer<PubSub> {
|
||||||
|
return Layer.effect(PubSub)(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const service = makePubSubService(backend);
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
service.close.pipe(
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.logError("[PubSub] Failed to close backend", {
|
||||||
|
error: error.message,
|
||||||
|
operation: error.operation,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return PubSub.of(service);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeNatsPubSubLayer(url = "nats://localhost:4222"): Layer.Layer<PubSub> {
|
||||||
|
return pubSubLayer(new NatsBackend(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NatsPubSubLive = Layer.effect(PubSub)(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const natsUrl = O.getOrUndefined(yield* Config.string("NATS_URL").pipe(Config.option));
|
||||||
|
const pulsarHost = O.getOrUndefined(yield* Config.string("PULSAR_HOST").pipe(Config.option));
|
||||||
|
const service = makePubSubService(new NatsBackend(natsUrl ?? pulsarHost ?? "nats://localhost:4222"));
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
service.close.pipe(
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.logError("[PubSub] Failed to close NATS backend", {
|
||||||
|
error: error.message,
|
||||||
|
operation: error.operation,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return PubSub.of(service);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
* (NATS, Pulsar, Redis Streams) implements these interfaces.
|
* (NATS, Pulsar, Redis Streams) implements these interfaces.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type * as S from "effect/Schema";
|
||||||
|
|
||||||
export interface Message<T = unknown> {
|
export interface Message<T = unknown> {
|
||||||
value(): T;
|
value(): T;
|
||||||
properties(): Record<string, string>;
|
properties(): Record<string, string>;
|
||||||
|
|
@ -29,6 +31,7 @@ export type InitialPosition = "latest" | "earliest";
|
||||||
|
|
||||||
export interface CreateProducerOptions {
|
export interface CreateProducerOptions {
|
||||||
topic: string;
|
topic: string;
|
||||||
|
schema?: S.Top;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateConsumerOptions {
|
export interface CreateConsumerOptions {
|
||||||
|
|
@ -36,6 +39,7 @@ export interface CreateConsumerOptions {
|
||||||
subscription: string;
|
subscription: string;
|
||||||
initialPosition?: InitialPosition;
|
initialPosition?: InitialPosition;
|
||||||
consumerType?: ConsumerType;
|
consumerType?: ConsumerType;
|
||||||
|
schema?: S.Top;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PubSubBackend {
|
export interface PubSubBackend {
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,310 @@
|
||||||
/**
|
/**
|
||||||
* Custom error types.
|
* Typed errors and wire-error translation helpers.
|
||||||
*
|
*
|
||||||
* Python reference: trustgraph-base/trustgraph/exceptions.py
|
* Python reference: trustgraph-base/trustgraph/exceptions.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class TooManyRequestsError extends Error {
|
import * as S from "effect/Schema";
|
||||||
constructor(message = "Rate limit exceeded") {
|
import type { TgError } from "./schema/primitives.js";
|
||||||
super(message);
|
|
||||||
this.name = "TooManyRequestsError";
|
export class TooManyRequestsError extends S.TaggedErrorClass<TooManyRequestsError>()(
|
||||||
}
|
"TooManyRequestsError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class LlmError extends S.TaggedErrorClass<LlmError>()(
|
||||||
|
"LlmError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
errorType: S.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class EmbeddingsError extends S.TaggedErrorClass<EmbeddingsError>()(
|
||||||
|
"EmbeddingsError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
operation: S.String,
|
||||||
|
provider: S.optionalKey(S.String),
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class ParseError extends S.TaggedErrorClass<ParseError>()(
|
||||||
|
"ParseError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class RuntimeConfigError extends S.TaggedErrorClass<RuntimeConfigError>()(
|
||||||
|
"RuntimeConfigError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
key: S.optionalKey(S.String),
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class WireDecodeError extends S.TaggedErrorClass<WireDecodeError>()(
|
||||||
|
"WireDecodeError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
service: S.optionalKey(S.String),
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class PubSubError extends S.TaggedErrorClass<PubSubError>()(
|
||||||
|
"PubSubError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
operation: S.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class ProcessorLifecycleError extends S.TaggedErrorClass<ProcessorLifecycleError>()(
|
||||||
|
"ProcessorLifecycleError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
operation: S.String,
|
||||||
|
processorId: S.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class MessagingLifecycleError extends S.TaggedErrorClass<MessagingLifecycleError>()(
|
||||||
|
"MessagingLifecycleError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
operation: S.String,
|
||||||
|
resource: S.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class MessagingDeliveryError extends S.TaggedErrorClass<MessagingDeliveryError>()(
|
||||||
|
"MessagingDeliveryError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
operation: S.String,
|
||||||
|
topic: S.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class MessagingDecodeError extends S.TaggedErrorClass<MessagingDecodeError>()(
|
||||||
|
"MessagingDecodeError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
operation: S.String,
|
||||||
|
topic: S.optionalKey(S.String),
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class MessagingTimeoutError extends S.TaggedErrorClass<MessagingTimeoutError>()(
|
||||||
|
"MessagingTimeoutError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
operation: S.String,
|
||||||
|
timeoutMs: S.Number,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class MessagingHandlerError extends S.TaggedErrorClass<MessagingHandlerError>()(
|
||||||
|
"MessagingHandlerError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
topic: S.String,
|
||||||
|
subscription: S.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class FlowRuntimeError extends S.TaggedErrorClass<FlowRuntimeError>()(
|
||||||
|
"FlowRuntimeError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
flowName: S.String,
|
||||||
|
operation: S.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class FlowResourceNotFoundError extends S.TaggedErrorClass<FlowResourceNotFoundError>()(
|
||||||
|
"FlowResourceNotFoundError",
|
||||||
|
{
|
||||||
|
message: S.String,
|
||||||
|
flowName: S.String,
|
||||||
|
resourceType: S.Union([
|
||||||
|
S.Literal("producer"),
|
||||||
|
S.Literal("consumer"),
|
||||||
|
S.Literal("requestor"),
|
||||||
|
S.Literal("parameter"),
|
||||||
|
]),
|
||||||
|
resourceName: S.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export type TrustGraphError =
|
||||||
|
| TooManyRequestsError
|
||||||
|
| LlmError
|
||||||
|
| EmbeddingsError
|
||||||
|
| ParseError
|
||||||
|
| RuntimeConfigError
|
||||||
|
| WireDecodeError
|
||||||
|
| PubSubError
|
||||||
|
| ProcessorLifecycleError
|
||||||
|
| MessagingLifecycleError
|
||||||
|
| MessagingDeliveryError
|
||||||
|
| MessagingDecodeError
|
||||||
|
| MessagingTimeoutError
|
||||||
|
| MessagingHandlerError
|
||||||
|
| FlowRuntimeError
|
||||||
|
| FlowResourceNotFoundError;
|
||||||
|
|
||||||
|
export type MessagingRuntimeError =
|
||||||
|
| PubSubError
|
||||||
|
| MessagingLifecycleError
|
||||||
|
| MessagingDeliveryError
|
||||||
|
| MessagingDecodeError
|
||||||
|
| MessagingTimeoutError
|
||||||
|
| MessagingHandlerError
|
||||||
|
| FlowRuntimeError
|
||||||
|
| FlowResourceNotFoundError;
|
||||||
|
|
||||||
|
export function tooManyRequestsError(message = "Rate limit exceeded"): TooManyRequestsError {
|
||||||
|
return new TooManyRequestsError({ message });
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LlmError extends Error {
|
export function llmError(message: string, errorType = "llm-error"): LlmError {
|
||||||
constructor(
|
return new LlmError({ message, errorType });
|
||||||
message: string,
|
|
||||||
public readonly errorType: string = "llm-error",
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
this.name = "LlmError";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ParseError extends Error {
|
export function embeddingsError(
|
||||||
constructor(message: string) {
|
operation: string,
|
||||||
super(message);
|
error: unknown,
|
||||||
this.name = "ParseError";
|
provider?: string,
|
||||||
}
|
): EmbeddingsError {
|
||||||
|
return new EmbeddingsError({
|
||||||
|
operation,
|
||||||
|
message: errorMessage(error),
|
||||||
|
...(provider === undefined ? {} : { provider }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseError(message: string): ParseError {
|
||||||
|
return new ParseError({ message });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pubSubError(operation: string, error: unknown): PubSubError {
|
||||||
|
return new PubSubError({ operation, message: errorMessage(error) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processorLifecycleError(
|
||||||
|
processorId: string,
|
||||||
|
operation: string,
|
||||||
|
error: unknown,
|
||||||
|
): ProcessorLifecycleError {
|
||||||
|
return new ProcessorLifecycleError({
|
||||||
|
processorId,
|
||||||
|
operation,
|
||||||
|
message: errorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messagingLifecycleError(
|
||||||
|
resource: string,
|
||||||
|
operation: string,
|
||||||
|
error: unknown,
|
||||||
|
): MessagingLifecycleError {
|
||||||
|
return new MessagingLifecycleError({
|
||||||
|
resource,
|
||||||
|
operation,
|
||||||
|
message: errorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messagingDeliveryError(
|
||||||
|
topic: string,
|
||||||
|
operation: string,
|
||||||
|
error: unknown,
|
||||||
|
): MessagingDeliveryError {
|
||||||
|
return new MessagingDeliveryError({
|
||||||
|
topic,
|
||||||
|
operation,
|
||||||
|
message: errorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messagingDecodeError(
|
||||||
|
operation: string,
|
||||||
|
error: unknown,
|
||||||
|
topic?: string,
|
||||||
|
): MessagingDecodeError {
|
||||||
|
return new MessagingDecodeError({
|
||||||
|
operation,
|
||||||
|
message: errorMessage(error),
|
||||||
|
...(topic === undefined ? {} : { topic }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messagingTimeoutError(
|
||||||
|
operation: string,
|
||||||
|
timeoutMs: number,
|
||||||
|
): MessagingTimeoutError {
|
||||||
|
return new MessagingTimeoutError({
|
||||||
|
operation,
|
||||||
|
timeoutMs,
|
||||||
|
message: `${operation} timed out after ${timeoutMs}ms`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messagingHandlerError(
|
||||||
|
topic: string,
|
||||||
|
subscription: string,
|
||||||
|
error: unknown,
|
||||||
|
): MessagingHandlerError {
|
||||||
|
return new MessagingHandlerError({
|
||||||
|
topic,
|
||||||
|
subscription,
|
||||||
|
message: errorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flowRuntimeError(
|
||||||
|
flowName: string,
|
||||||
|
operation: string,
|
||||||
|
error: unknown,
|
||||||
|
): FlowRuntimeError {
|
||||||
|
return new FlowRuntimeError({
|
||||||
|
flowName,
|
||||||
|
operation,
|
||||||
|
message: errorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flowResourceNotFoundError(
|
||||||
|
flowName: string,
|
||||||
|
resourceType: FlowResourceNotFoundError["resourceType"],
|
||||||
|
resourceName: string,
|
||||||
|
): FlowResourceNotFoundError {
|
||||||
|
return new FlowResourceNotFoundError({
|
||||||
|
flowName,
|
||||||
|
resourceType,
|
||||||
|
resourceName,
|
||||||
|
message: `${resourceType} "${resourceName}" not found in flow "${flowName}"`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorMessage(error: unknown): string {
|
||||||
|
if (typeof error === "object" && error !== null && "message" in error) {
|
||||||
|
const message = (error as { message?: unknown }).message;
|
||||||
|
if (typeof message === "string") return message;
|
||||||
|
}
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toTgError(error: unknown, fallbackType = "internal"): TgError {
|
||||||
|
if (typeof error === "object" && error !== null && "_tag" in error) {
|
||||||
|
const tag = (error as { _tag?: unknown })._tag;
|
||||||
|
if (typeof tag === "string") {
|
||||||
|
return { type: tag, message: errorMessage(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { type: fallbackType, message: errorMessage(error) };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,5 @@ export * from "./schema/index.js";
|
||||||
export * from "./spec/index.js";
|
export * from "./spec/index.js";
|
||||||
export * from "./services/index.js";
|
export * from "./services/index.js";
|
||||||
export * from "./metrics/index.js";
|
export * from "./metrics/index.js";
|
||||||
|
export * from "./runtime/index.js";
|
||||||
export * from "./errors.js";
|
export * from "./errors.js";
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import type { PubSubBackend, BackendConsumer, Message } from "../backend/types.js";
|
import type { PubSubBackend, BackendConsumer, Message } from "../backend/types.js";
|
||||||
import type { Flow } from "../processor/flow.js";
|
import type { Flow } from "../processor/flow.js";
|
||||||
import { TooManyRequestsError } from "../errors.js";
|
import { TooManyRequestsError } from "../errors.js";
|
||||||
|
import * as S from "effect/Schema";
|
||||||
|
|
||||||
export type MessageHandler<T> = (
|
export type MessageHandler<T> = (
|
||||||
message: T,
|
message: T,
|
||||||
|
|
@ -14,11 +15,11 @@ export type MessageHandler<T> = (
|
||||||
flow: FlowContext,
|
flow: FlowContext,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
export interface FlowContext {
|
export interface FlowContext<Requirements = never> {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
/** Reference to the owning Flow instance, giving handlers access to producers and parameters. */
|
/** Reference to the owning Flow instance, giving handlers access to producers and parameters. */
|
||||||
flow: Flow;
|
flow: Flow<Requirements>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConsumerOptions<T> {
|
export interface ConsumerOptions<T> {
|
||||||
|
|
@ -36,11 +37,13 @@ export class Consumer<T> {
|
||||||
private backend: BackendConsumer<T> | null = null;
|
private backend: BackendConsumer<T> | null = null;
|
||||||
private running = false;
|
private running = false;
|
||||||
private abortController = new AbortController();
|
private abortController = new AbortController();
|
||||||
|
private readonly options: ConsumerOptions<T>;
|
||||||
|
|
||||||
private readonly concurrency: number;
|
private readonly concurrency: number;
|
||||||
private readonly rateLimitRetryMs: number;
|
private readonly rateLimitRetryMs: number;
|
||||||
|
|
||||||
constructor(private readonly options: ConsumerOptions<T>) {
|
constructor(options: ConsumerOptions<T>) {
|
||||||
|
this.options = options;
|
||||||
this.concurrency = options.concurrency ?? 1;
|
this.concurrency = options.concurrency ?? 1;
|
||||||
this.rateLimitRetryMs = options.rateLimitRetryMs ?? 10_000;
|
this.rateLimitRetryMs = options.rateLimitRetryMs ?? 10_000;
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +68,7 @@ export class Consumer<T> {
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
this.abortController.abort();
|
this.abortController.abort();
|
||||||
if (this.backend) {
|
if (this.backend !== null) {
|
||||||
await this.backend.close();
|
await this.backend.close();
|
||||||
this.backend = null;
|
this.backend = null;
|
||||||
}
|
}
|
||||||
|
|
@ -75,17 +78,23 @@ export class Consumer<T> {
|
||||||
while (this.running) {
|
while (this.running) {
|
||||||
let msg: Message<T> | null = null;
|
let msg: Message<T> | null = null;
|
||||||
try {
|
try {
|
||||||
msg = await this.backend!.receive(2000);
|
const backend = this.backend;
|
||||||
if (!msg) continue;
|
if (backend === null) throw new Error("Consumer backend not started");
|
||||||
|
|
||||||
|
msg = await backend.receive(2000);
|
||||||
|
if (msg === null) continue;
|
||||||
|
|
||||||
await this.handleWithRetry(msg, flow);
|
await this.handleWithRetry(msg, flow);
|
||||||
await this.backend!.acknowledge(msg);
|
await backend.acknowledge(msg);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!this.running) break;
|
if (!this.running) break;
|
||||||
console.error("[Consumer] Error in consume loop:", err);
|
console.error("[Consumer] Error in consume loop:", err);
|
||||||
if (msg) {
|
if (msg !== null) {
|
||||||
try {
|
try {
|
||||||
await this.backend!.negativeAcknowledge(msg);
|
const backend = this.backend;
|
||||||
|
if (backend !== null) {
|
||||||
|
await backend.negativeAcknowledge(msg);
|
||||||
|
}
|
||||||
} catch (nakErr) {
|
} catch (nakErr) {
|
||||||
console.error("[Consumer] Failed to nak message:", nakErr);
|
console.error("[Consumer] Failed to nak message:", nakErr);
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +108,7 @@ export class Consumer<T> {
|
||||||
try {
|
try {
|
||||||
await this.options.handler(msg.value(), msg.properties(), flow);
|
await this.options.handler(msg.value(), msg.properties(), flow);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TooManyRequestsError) {
|
if (S.is(TooManyRequestsError)(err)) {
|
||||||
console.warn(`[Consumer] Rate limited, retrying in ${this.rateLimitRetryMs}ms`);
|
console.warn(`[Consumer] Rate limited, retrying in ${this.rateLimitRetryMs}ms`);
|
||||||
await sleep(this.rateLimitRetryMs);
|
await sleep(this.rateLimitRetryMs);
|
||||||
await this.options.handler(msg.value(), msg.properties(), flow);
|
await this.options.handler(msg.value(), msg.properties(), flow);
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,37 @@ export { Producer } from "./producer.js";
|
||||||
export { Consumer, type MessageHandler, type FlowContext, type ConsumerOptions } from "./consumer.js";
|
export { Consumer, type MessageHandler, type FlowContext, type ConsumerOptions } from "./consumer.js";
|
||||||
export { Subscriber, AsyncQueue } from "./subscriber.js";
|
export { Subscriber, AsyncQueue } from "./subscriber.js";
|
||||||
export { RequestResponse, type RequestResponseOptions } from "./request-response.js";
|
export { RequestResponse, type RequestResponseOptions } from "./request-response.js";
|
||||||
|
export {
|
||||||
|
ConsumerFactory,
|
||||||
|
ConsumerFactoryLive,
|
||||||
|
FlowRuntime,
|
||||||
|
FlowRuntimeLive,
|
||||||
|
MessagingRuntimeLive,
|
||||||
|
ProducerFactory,
|
||||||
|
ProducerFactoryLive,
|
||||||
|
RequestResponseFactory,
|
||||||
|
RequestResponseFactoryLive,
|
||||||
|
makeEffectConsumerFromPubSub,
|
||||||
|
makeEffectProducerFromPubSub,
|
||||||
|
makeEffectProducerHandle,
|
||||||
|
makeEffectRequestResponseFromPubSub,
|
||||||
|
makeConsumerFactoryService,
|
||||||
|
makeProducerFactoryService,
|
||||||
|
makeRequestResponseFactoryService,
|
||||||
|
runEffectConsumerScoped,
|
||||||
|
runEffectProducerScoped,
|
||||||
|
runEffectRequestResponseScoped,
|
||||||
|
runFlowScoped,
|
||||||
|
type ConsumerFactoryService,
|
||||||
|
type EffectConsumer,
|
||||||
|
type EffectConsumerOptions,
|
||||||
|
type EffectMessageHandler,
|
||||||
|
type EffectProducer,
|
||||||
|
type EffectProducerOptions,
|
||||||
|
type EffectRequestOptions,
|
||||||
|
type EffectRequestResponse,
|
||||||
|
type EffectRequestResponseOptions,
|
||||||
|
type FlowRuntimeService,
|
||||||
|
type ProducerFactoryService,
|
||||||
|
type RequestResponseFactoryService,
|
||||||
|
} from "./runtime.js";
|
||||||
|
|
|
||||||
|
|
@ -6,34 +6,44 @@
|
||||||
|
|
||||||
import type { PubSubBackend, BackendProducer } from "../backend/types.js";
|
import type { PubSubBackend, BackendProducer } from "../backend/types.js";
|
||||||
import type { ProducerMetrics } from "../metrics/prometheus.js";
|
import type { ProducerMetrics } from "../metrics/prometheus.js";
|
||||||
|
import { Effect } from "effect";
|
||||||
|
import { makeEffectProducerHandle, type EffectProducer } from "./runtime.js";
|
||||||
|
|
||||||
export class Producer<T> {
|
export class Producer<T> {
|
||||||
private backend: BackendProducer<T> | null = null;
|
private backend: BackendProducer<T> | null = null;
|
||||||
private running = false;
|
private effectProducer: EffectProducer<T> | null = null;
|
||||||
|
private readonly pubsub: PubSubBackend;
|
||||||
|
private readonly topic: string;
|
||||||
|
private readonly metrics: ProducerMetrics | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(pubsub: PubSubBackend, topic: string, metrics?: ProducerMetrics) {
|
||||||
private readonly pubsub: PubSubBackend,
|
this.pubsub = pubsub;
|
||||||
private readonly topic: string,
|
this.topic = topic;
|
||||||
private readonly metrics?: ProducerMetrics,
|
this.metrics = metrics;
|
||||||
) {}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
this.backend = await this.pubsub.createProducer<T>({ topic: this.topic });
|
this.backend = await this.pubsub.createProducer<T>({ topic: this.topic });
|
||||||
this.running = true;
|
this.effectProducer = makeEffectProducerHandle(this.backend, {
|
||||||
|
topic: this.topic,
|
||||||
|
...(this.metrics === undefined ? {} : { metrics: this.metrics }),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(id: string, message: T): Promise<void> {
|
async send(id: string, message: T): Promise<void> {
|
||||||
if (!this.backend) throw new Error("Producer not started");
|
if (this.effectProducer === null) throw new Error("Producer not started");
|
||||||
|
|
||||||
await this.backend.send(message, { id });
|
await Effect.runPromise(this.effectProducer.send(id, message));
|
||||||
this.metrics?.inc();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
this.running = false;
|
if (this.effectProducer !== null) {
|
||||||
if (this.backend) {
|
await Effect.runPromise(
|
||||||
await this.backend.flush();
|
this.effectProducer.flush.pipe(
|
||||||
await this.backend.close();
|
Effect.flatMap(() => this.effectProducer === null ? Effect.void : this.effectProducer.close),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.effectProducer = null;
|
||||||
this.backend = null;
|
this.backend = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export class RequestResponse<TReq, TRes> {
|
||||||
private producer: Producer<TReq>;
|
private producer: Producer<TReq>;
|
||||||
private subscriber: Subscriber<TRes>;
|
private subscriber: Subscriber<TRes>;
|
||||||
|
|
||||||
constructor(private readonly options: RequestResponseOptions) {
|
constructor(options: RequestResponseOptions) {
|
||||||
this.producer = new Producer<TReq>(options.pubsub, options.requestTopic);
|
this.producer = new Producer<TReq>(options.pubsub, options.requestTopic);
|
||||||
this.subscriber = new Subscriber<TRes>(
|
this.subscriber = new Subscriber<TRes>(
|
||||||
options.pubsub,
|
options.pubsub,
|
||||||
|
|
@ -77,7 +77,7 @@ export class RequestResponse<TReq, TRes> {
|
||||||
|
|
||||||
const response = await queue.pop(remaining);
|
const response = await queue.pop(remaining);
|
||||||
|
|
||||||
if (recipient) {
|
if (recipient !== undefined) {
|
||||||
const isFinal = await recipient(response);
|
const isFinal = await recipient(response);
|
||||||
if (isFinal) return response;
|
if (isFinal) return response;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
612
ts/packages/base/src/messaging/runtime.ts
Normal file
612
ts/packages/base/src/messaging/runtime.ts
Normal file
|
|
@ -0,0 +1,612 @@
|
||||||
|
/**
|
||||||
|
* Effect-native messaging factories and scoped runtime helpers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { Context, Duration, Effect, Fiber, Layer, Queue, Scope } from "effect";
|
||||||
|
import * as O from "effect/Option";
|
||||||
|
import * as S from "effect/Schema";
|
||||||
|
import type {
|
||||||
|
BackendConsumer,
|
||||||
|
BackendProducer,
|
||||||
|
CreateConsumerOptions,
|
||||||
|
CreateProducerOptions,
|
||||||
|
Message,
|
||||||
|
} from "../backend/types.js";
|
||||||
|
import { PubSub, type PubSubService } from "../backend/pubsub.js";
|
||||||
|
import {
|
||||||
|
flowRuntimeError,
|
||||||
|
messagingDeliveryError,
|
||||||
|
messagingHandlerError,
|
||||||
|
messagingLifecycleError,
|
||||||
|
messagingTimeoutError,
|
||||||
|
TooManyRequestsError,
|
||||||
|
type FlowRuntimeError,
|
||||||
|
type MessagingDeliveryError,
|
||||||
|
type MessagingLifecycleError,
|
||||||
|
type MessagingTimeoutError,
|
||||||
|
type PubSubError,
|
||||||
|
} from "../errors.js";
|
||||||
|
import type { ProducerMetrics } from "../metrics/prometheus.js";
|
||||||
|
import type { FlowContext } from "./consumer.js";
|
||||||
|
import type { Flow } from "../processor/flow.js";
|
||||||
|
import type { SpecRuntimeRequirements } from "../spec/types.js";
|
||||||
|
import {
|
||||||
|
loadMessagingRuntimeConfig,
|
||||||
|
type MessagingRuntimeConfig,
|
||||||
|
} from "../runtime/messaging-config.js";
|
||||||
|
|
||||||
|
const isTooManyRequestsError = S.is(TooManyRequestsError);
|
||||||
|
|
||||||
|
export type EffectMessageHandler<T, E = never, R = never> = (
|
||||||
|
message: T,
|
||||||
|
properties: Record<string, string>,
|
||||||
|
flow: FlowContext<R>,
|
||||||
|
) => Effect.Effect<void, E, R>;
|
||||||
|
|
||||||
|
export interface EffectProducerOptions {
|
||||||
|
readonly topic: string;
|
||||||
|
readonly schema?: S.Top;
|
||||||
|
readonly metrics?: ProducerMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EffectProducer<T> {
|
||||||
|
readonly send: (id: string, message: T) => Effect.Effect<void, MessagingDeliveryError>;
|
||||||
|
readonly flush: Effect.Effect<void, MessagingDeliveryError>;
|
||||||
|
readonly close: Effect.Effect<void, MessagingDeliveryError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EffectConsumerOptions<T, E = never, R = never> {
|
||||||
|
readonly topic: string;
|
||||||
|
readonly subscription: string;
|
||||||
|
readonly handler: EffectMessageHandler<T, E, R>;
|
||||||
|
readonly concurrency?: number;
|
||||||
|
readonly initialPosition?: "latest" | "earliest";
|
||||||
|
readonly schema?: S.Top;
|
||||||
|
readonly receiveTimeoutMs?: number;
|
||||||
|
readonly errorBackoffMs?: number;
|
||||||
|
readonly rateLimitRetryMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EffectConsumer {
|
||||||
|
readonly stop: Effect.Effect<void, MessagingLifecycleError>;
|
||||||
|
readonly fibers: ReadonlyArray<Fiber.Fiber<void, never>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EffectRequestResponseOptions {
|
||||||
|
readonly requestTopic: string;
|
||||||
|
readonly responseTopic: string;
|
||||||
|
readonly subscription: string;
|
||||||
|
readonly requestSchema?: S.Top;
|
||||||
|
readonly responseSchema?: S.Top;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EffectRequestOptions<TRes, E = never, R = never> {
|
||||||
|
readonly timeoutMs?: number;
|
||||||
|
readonly recipient?: (response: TRes) => Effect.Effect<boolean, E, R>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EffectRequestResponse<TReq, TRes> {
|
||||||
|
readonly request: <E = never, R = never>(
|
||||||
|
request: TReq,
|
||||||
|
options?: EffectRequestOptions<TRes, E, R>,
|
||||||
|
) => Effect.Effect<TRes, MessagingDeliveryError | MessagingTimeoutError | E, R>;
|
||||||
|
readonly stop: Effect.Effect<void, MessagingLifecycleError | MessagingDeliveryError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProducerFactoryService {
|
||||||
|
readonly make: <T>(
|
||||||
|
options: EffectProducerOptions,
|
||||||
|
) => Effect.Effect<EffectProducer<T>, PubSubError, Scope.Scope>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConsumerFactoryService {
|
||||||
|
readonly run: <T, E = never, R = never>(
|
||||||
|
options: EffectConsumerOptions<T, E, R>,
|
||||||
|
flow: FlowContext<R>,
|
||||||
|
) => Effect.Effect<EffectConsumer, PubSubError, Scope.Scope | R>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestResponseFactoryService {
|
||||||
|
readonly make: <TReq, TRes>(
|
||||||
|
options: EffectRequestResponseOptions,
|
||||||
|
) => Effect.Effect<EffectRequestResponse<TReq, TRes>, PubSubError, Scope.Scope>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowRuntimeService {
|
||||||
|
readonly run: <Requirements = never>(
|
||||||
|
flow: Flow<Requirements>,
|
||||||
|
) => Effect.Effect<void, FlowRuntimeError, SpecRuntimeRequirements | Requirements>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProducerFactory extends Context.Service<ProducerFactory, ProducerFactoryService>()(
|
||||||
|
"@trustgraph/base/messaging/runtime/ProducerFactory",
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class ConsumerFactory extends Context.Service<ConsumerFactory, ConsumerFactoryService>()(
|
||||||
|
"@trustgraph/base/messaging/runtime/ConsumerFactory",
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class RequestResponseFactory extends Context.Service<
|
||||||
|
RequestResponseFactory,
|
||||||
|
RequestResponseFactoryService
|
||||||
|
>()("@trustgraph/base/messaging/runtime/RequestResponseFactory") {}
|
||||||
|
|
||||||
|
export class FlowRuntime extends Context.Service<FlowRuntime, FlowRuntimeService>()(
|
||||||
|
"@trustgraph/base/messaging/runtime/FlowRuntime",
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export function makeEffectProducerHandle<T>(
|
||||||
|
backend: BackendProducer<T>,
|
||||||
|
options: EffectProducerOptions,
|
||||||
|
): EffectProducer<T> {
|
||||||
|
return {
|
||||||
|
send: Effect.fn(`Producer.send:${options.topic}`)((id: string, message: T) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => backend.send(message, { id }),
|
||||||
|
catch: (error) => messagingDeliveryError(options.topic, "send", error),
|
||||||
|
}).pipe(
|
||||||
|
Effect.tap(() =>
|
||||||
|
options.metrics === undefined
|
||||||
|
? Effect.void
|
||||||
|
: Effect.sync(() => {
|
||||||
|
options.metrics?.inc();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flush: Effect.tryPromise({
|
||||||
|
try: () => backend.flush(),
|
||||||
|
catch: (error) => messagingDeliveryError(options.topic, "flush", error),
|
||||||
|
}),
|
||||||
|
close: Effect.tryPromise({
|
||||||
|
try: () => backend.close(),
|
||||||
|
catch: (error) => messagingDeliveryError(options.topic, "close", error),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeEffectProducerFromPubSub = Effect.fn("makeEffectProducerFromPubSub")(function* <T>(
|
||||||
|
pubsub: PubSubService,
|
||||||
|
options: EffectProducerOptions,
|
||||||
|
) {
|
||||||
|
const createOptions: CreateProducerOptions = options.schema === undefined
|
||||||
|
? { topic: options.topic }
|
||||||
|
: { topic: options.topic, schema: options.schema };
|
||||||
|
const backend = yield* pubsub.createProducer<T>(createOptions);
|
||||||
|
const producer = makeEffectProducerHandle(backend, options);
|
||||||
|
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
producer.close.pipe(
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.logError("[Producer] Failed to close producer", {
|
||||||
|
error: error.message,
|
||||||
|
topic: error.topic,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return producer;
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeConsumerBackend = <T>(
|
||||||
|
backend: BackendConsumer<T>,
|
||||||
|
topic: string,
|
||||||
|
subscription: string,
|
||||||
|
) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => backend.close(),
|
||||||
|
catch: (error) => messagingLifecycleError(`${topic}:${subscription}`, "close-consumer", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
const acknowledgeMessage = <T>(
|
||||||
|
backend: BackendConsumer<T>,
|
||||||
|
message: Message<T>,
|
||||||
|
topic: string,
|
||||||
|
) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => backend.acknowledge(message),
|
||||||
|
catch: (error) => messagingDeliveryError(topic, "acknowledge", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
const negativeAcknowledgeMessage = <T>(
|
||||||
|
backend: BackendConsumer<T>,
|
||||||
|
message: Message<T>,
|
||||||
|
topic: string,
|
||||||
|
) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => backend.negativeAcknowledge(message),
|
||||||
|
catch: (error) => messagingDeliveryError(topic, "negative-acknowledge", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
const receiveMessage = <T>(
|
||||||
|
backend: BackendConsumer<T>,
|
||||||
|
topic: string,
|
||||||
|
timeoutMs: number,
|
||||||
|
) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => backend.receive(timeoutMs),
|
||||||
|
catch: (error) => messagingDeliveryError(topic, "receive", error),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMessageWithRetry = Effect.fn("handleMessageWithRetry")(function* <T, E, R>(
|
||||||
|
options: EffectConsumerOptions<T, E, R>,
|
||||||
|
flow: FlowContext<R>,
|
||||||
|
message: Message<T>,
|
||||||
|
config: MessagingRuntimeConfig,
|
||||||
|
) {
|
||||||
|
const runHandler = Effect.fn(`Consumer.handler:${options.topic}`)(() =>
|
||||||
|
options.handler(message.value(), message.properties(), flow).pipe(
|
||||||
|
Effect.mapError((error) => messagingHandlerError(options.topic, options.subscription, error)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return yield* options.handler(message.value(), message.properties(), flow).pipe(
|
||||||
|
Effect.catch((error) => {
|
||||||
|
if (isTooManyRequestsError(error)) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* Effect.logWarning("[Consumer] Rate limited, retrying", {
|
||||||
|
topic: options.topic,
|
||||||
|
subscription: options.subscription,
|
||||||
|
retryMs: config.rateLimitRetryMs,
|
||||||
|
});
|
||||||
|
yield* Effect.sleep(Duration.millis(config.rateLimitRetryMs));
|
||||||
|
yield* runHandler();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Effect.fail(messagingHandlerError(options.topic, options.subscription, error));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const processConsumerMessage = Effect.fn("processConsumerMessage")(function* <T, E, R>(
|
||||||
|
backend: BackendConsumer<T>,
|
||||||
|
options: EffectConsumerOptions<T, E, R>,
|
||||||
|
flow: FlowContext<R>,
|
||||||
|
message: Message<T>,
|
||||||
|
config: MessagingRuntimeConfig,
|
||||||
|
) {
|
||||||
|
yield* handleMessageWithRetry(options, flow, message, config).pipe(
|
||||||
|
Effect.flatMap(() => acknowledgeMessage(backend, message, options.topic)),
|
||||||
|
Effect.catch((error) =>
|
||||||
|
negativeAcknowledgeMessage(backend, message, options.topic).pipe(
|
||||||
|
Effect.catch((nakError) =>
|
||||||
|
Effect.logError("[Consumer] Failed to negative-acknowledge message", {
|
||||||
|
error: nakError.message,
|
||||||
|
topic: nakError.topic,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Effect.flatMap(() =>
|
||||||
|
Effect.logError("[Consumer] Message handling failed", {
|
||||||
|
error: error.message,
|
||||||
|
topic: options.topic,
|
||||||
|
subscription: options.subscription,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const consumerLoop = <T, E, R>(
|
||||||
|
backend: BackendConsumer<T>,
|
||||||
|
options: EffectConsumerOptions<T, E, R>,
|
||||||
|
flow: FlowContext<R>,
|
||||||
|
config: MessagingRuntimeConfig,
|
||||||
|
): Effect.Effect<void, never, R> =>
|
||||||
|
Effect.whileLoop({
|
||||||
|
while: () => true,
|
||||||
|
body: () =>
|
||||||
|
receiveMessage(backend, options.topic, options.receiveTimeoutMs ?? config.consumerReceiveTimeoutMs).pipe(
|
||||||
|
Effect.flatMap((message) =>
|
||||||
|
message === null
|
||||||
|
? Effect.sleep(Duration.millis(options.receiveTimeoutMs ?? config.consumerReceiveTimeoutMs))
|
||||||
|
: processConsumerMessage(backend, options, flow, message, config),
|
||||||
|
),
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.logError("[Consumer] Receive loop failed", {
|
||||||
|
error: error.message,
|
||||||
|
topic: options.topic,
|
||||||
|
subscription: options.subscription,
|
||||||
|
}).pipe(
|
||||||
|
Effect.flatMap(() =>
|
||||||
|
Effect.sleep(Duration.millis(options.errorBackoffMs ?? config.consumerErrorBackoffMs)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
step: () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const makeEffectConsumerFromPubSub = Effect.fn("makeEffectConsumerFromPubSub")(function* <T, E, R>(
|
||||||
|
pubsub: PubSubService,
|
||||||
|
config: MessagingRuntimeConfig,
|
||||||
|
options: EffectConsumerOptions<T, E, R>,
|
||||||
|
flow: FlowContext<R>,
|
||||||
|
) {
|
||||||
|
const createOptions: CreateConsumerOptions = {
|
||||||
|
topic: options.topic,
|
||||||
|
subscription: options.subscription,
|
||||||
|
...(options.initialPosition === undefined ? {} : { initialPosition: options.initialPosition }),
|
||||||
|
...(options.schema === undefined ? {} : { schema: options.schema }),
|
||||||
|
};
|
||||||
|
const backend = yield* pubsub.createConsumer<T>(createOptions);
|
||||||
|
const concurrency = Math.max(1, options.concurrency ?? 1);
|
||||||
|
const workerIndexes = Array.from({ length: concurrency }, (_value, index) => index);
|
||||||
|
const fibers = yield* Effect.forEach(workerIndexes, () =>
|
||||||
|
consumerLoop(backend, options, flow, {
|
||||||
|
...config,
|
||||||
|
rateLimitRetryMs: options.rateLimitRetryMs ?? config.rateLimitRetryMs,
|
||||||
|
}).pipe(Effect.forkChild),
|
||||||
|
);
|
||||||
|
|
||||||
|
const stop = Effect.fn(`Consumer.stop:${options.topic}`)(function* () {
|
||||||
|
yield* Effect.forEach(fibers, Fiber.interrupt, { discard: true });
|
||||||
|
yield* closeConsumerBackend(backend, options.topic, options.subscription);
|
||||||
|
});
|
||||||
|
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
stop().pipe(
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.logError("[Consumer] Failed to stop consumer", {
|
||||||
|
error: error.message,
|
||||||
|
resource: error.resource,
|
||||||
|
operation: error.operation,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fibers,
|
||||||
|
stop: stop(),
|
||||||
|
} satisfies EffectConsumer;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dispatchResponseLoop = <T>(
|
||||||
|
backend: BackendConsumer<T>,
|
||||||
|
responseTopic: string,
|
||||||
|
subscribers: Map<string, Queue.Queue<T>>,
|
||||||
|
config: MessagingRuntimeConfig,
|
||||||
|
): Effect.Effect<void> =>
|
||||||
|
Effect.whileLoop({
|
||||||
|
while: () => true,
|
||||||
|
body: () =>
|
||||||
|
receiveMessage(backend, responseTopic, config.consumerReceiveTimeoutMs).pipe(
|
||||||
|
Effect.flatMap((message) => {
|
||||||
|
if (message === null) {
|
||||||
|
return Effect.sleep(Duration.millis(config.consumerReceiveTimeoutMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = message.properties().id;
|
||||||
|
const queue = id === undefined ? undefined : subscribers.get(id);
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
if (queue !== undefined) {
|
||||||
|
yield* Queue.offer(queue, message.value());
|
||||||
|
}
|
||||||
|
yield* acknowledgeMessage(backend, message, responseTopic);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.logError("[RequestResponse] Response dispatch failed", {
|
||||||
|
error: error.message,
|
||||||
|
topic: responseTopic,
|
||||||
|
}).pipe(Effect.flatMap(() => Effect.sleep(Duration.millis(config.consumerErrorBackoffMs)))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
step: () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitForResponse = Effect.fn("waitForResponse")(function* <TRes, E, R>(
|
||||||
|
queue: Queue.Queue<TRes>,
|
||||||
|
options: EffectRequestOptions<TRes, E, R> | undefined,
|
||||||
|
) {
|
||||||
|
while (true) {
|
||||||
|
const response = yield* Queue.take(queue);
|
||||||
|
if (options?.recipient === undefined) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const complete = yield* options.recipient(response);
|
||||||
|
if (complete) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const makeEffectRequestResponseFromPubSub = Effect.fn("makeEffectRequestResponseFromPubSub")(function* <
|
||||||
|
TReq,
|
||||||
|
TRes,
|
||||||
|
>(
|
||||||
|
pubsub: PubSubService,
|
||||||
|
config: MessagingRuntimeConfig,
|
||||||
|
options: EffectRequestResponseOptions,
|
||||||
|
) {
|
||||||
|
const producer = yield* makeEffectProducerFromPubSub<TReq>(pubsub, {
|
||||||
|
topic: options.requestTopic,
|
||||||
|
...(options.requestSchema === undefined ? {} : { schema: options.requestSchema }),
|
||||||
|
});
|
||||||
|
const createOptions: CreateConsumerOptions = {
|
||||||
|
topic: options.responseTopic,
|
||||||
|
subscription: options.subscription,
|
||||||
|
...(options.responseSchema === undefined ? {} : { schema: options.responseSchema }),
|
||||||
|
};
|
||||||
|
const backend = yield* pubsub.createConsumer<TRes>(createOptions);
|
||||||
|
const subscribers = new Map<string, Queue.Queue<TRes>>();
|
||||||
|
const fiber = yield* dispatchResponseLoop(backend, options.responseTopic, subscribers, config).pipe(Effect.forkChild);
|
||||||
|
|
||||||
|
const stop = Effect.fn(`RequestResponse.stop:${options.requestTopic}`)(function* () {
|
||||||
|
yield* Fiber.interrupt(fiber);
|
||||||
|
yield* producer.close;
|
||||||
|
yield* closeConsumerBackend(backend, options.responseTopic, options.subscription);
|
||||||
|
});
|
||||||
|
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
stop().pipe(
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.logError("[RequestResponse] Failed to stop runtime", {
|
||||||
|
error: error.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
request: <E = never, R = never>(
|
||||||
|
request: TReq,
|
||||||
|
requestOptions?: EffectRequestOptions<TRes, E, R>,
|
||||||
|
) => {
|
||||||
|
const id = randomUUID();
|
||||||
|
const timeoutMs = requestOptions?.timeoutMs ?? config.requestTimeoutMs;
|
||||||
|
|
||||||
|
return Effect.acquireUseRelease(
|
||||||
|
Queue.unbounded<TRes>().pipe(
|
||||||
|
Effect.tap((queue) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
subscribers.set(id, queue);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(queue) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* producer.send(id, request);
|
||||||
|
const result = yield* waitForResponse(queue, requestOptions).pipe(
|
||||||
|
Effect.timeoutOption(Duration.millis(timeoutMs)),
|
||||||
|
);
|
||||||
|
return yield* O.match(result, {
|
||||||
|
onNone: () => Effect.fail(messagingTimeoutError("request-response", timeoutMs)),
|
||||||
|
onSome: Effect.succeed,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
(queue) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
subscribers.delete(id);
|
||||||
|
}).pipe(
|
||||||
|
Effect.flatMap(() => Queue.shutdown(queue)),
|
||||||
|
Effect.ignore,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
stop: stop(),
|
||||||
|
} satisfies EffectRequestResponse<TReq, TRes>;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function makeProducerFactoryService(pubsub: PubSubService): ProducerFactoryService {
|
||||||
|
return {
|
||||||
|
make: Effect.fn("ProducerFactory.make")(<T>(options: EffectProducerOptions) =>
|
||||||
|
makeEffectProducerFromPubSub<T>(pubsub, options),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeConsumerFactoryService(
|
||||||
|
pubsub: PubSubService,
|
||||||
|
config: MessagingRuntimeConfig,
|
||||||
|
): ConsumerFactoryService {
|
||||||
|
return {
|
||||||
|
run: Effect.fn("ConsumerFactory.run")(<T, E = never, R = never>(
|
||||||
|
options: EffectConsumerOptions<T, E, R>,
|
||||||
|
flow: FlowContext<R>,
|
||||||
|
) =>
|
||||||
|
makeEffectConsumerFromPubSub(pubsub, config, options, flow),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeRequestResponseFactoryService(
|
||||||
|
pubsub: PubSubService,
|
||||||
|
config: MessagingRuntimeConfig,
|
||||||
|
): RequestResponseFactoryService {
|
||||||
|
const make = Effect.fn("RequestResponseFactory.make")(function* <TReq, TRes>(
|
||||||
|
options: EffectRequestResponseOptions,
|
||||||
|
) {
|
||||||
|
return yield* makeEffectRequestResponseFromPubSub<TReq, TRes>(pubsub, config, options);
|
||||||
|
}) as RequestResponseFactoryService["make"];
|
||||||
|
|
||||||
|
return { make };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProducerFactoryLive = Layer.effect(
|
||||||
|
ProducerFactory,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
return ProducerFactory.of(makeProducerFactoryService(pubsub));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ConsumerFactoryLive = Layer.effect(
|
||||||
|
ConsumerFactory,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
const config = yield* loadMessagingRuntimeConfig();
|
||||||
|
return ConsumerFactory.of(makeConsumerFactoryService(pubsub, config));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const RequestResponseFactoryLive = Layer.effect(
|
||||||
|
RequestResponseFactory,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
const config = yield* loadMessagingRuntimeConfig();
|
||||||
|
return RequestResponseFactory.of(makeRequestResponseFactoryService(pubsub, config));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const runFlowRuntimeScoped = Effect.fn("FlowRuntime.run")(function* <Requirements = never>(
|
||||||
|
flow: Flow<Requirements>,
|
||||||
|
) {
|
||||||
|
yield* flow.startEffect().pipe(
|
||||||
|
Effect.mapError((error) => flowRuntimeError(flow.name, "start", error)),
|
||||||
|
);
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
flow.clearResources();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const FlowRuntimeLive = Layer.succeed(
|
||||||
|
FlowRuntime,
|
||||||
|
FlowRuntime.of({
|
||||||
|
run: runFlowRuntimeScoped,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MessagingRuntimeLive = Layer.mergeAll(
|
||||||
|
ProducerFactoryLive,
|
||||||
|
ConsumerFactoryLive,
|
||||||
|
RequestResponseFactoryLive,
|
||||||
|
FlowRuntimeLive,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const runEffectProducerScoped = Effect.fn("runEffectProducerScoped")(function* <T>(
|
||||||
|
options: EffectProducerOptions,
|
||||||
|
) {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
return yield* makeEffectProducerFromPubSub<T>(pubsub, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const runEffectConsumerScoped = Effect.fn("runEffectConsumerScoped")(function* <T, E = never, R = never>(
|
||||||
|
options: EffectConsumerOptions<T, E, R>,
|
||||||
|
flow: FlowContext<R>,
|
||||||
|
) {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
const config = yield* loadMessagingRuntimeConfig();
|
||||||
|
return yield* makeEffectConsumerFromPubSub(pubsub, config, options, flow);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const runEffectRequestResponseScoped = Effect.fn("runEffectRequestResponseScoped")(function* <TReq, TRes>(
|
||||||
|
options: EffectRequestResponseOptions,
|
||||||
|
) {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
const config = yield* loadMessagingRuntimeConfig();
|
||||||
|
return yield* makeEffectRequestResponseFromPubSub<TReq, TRes>(pubsub, config, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const runFlowScoped = Effect.fn("runFlowScoped")(function* (
|
||||||
|
flow: Flow,
|
||||||
|
) {
|
||||||
|
yield* runFlowRuntimeScoped(flow);
|
||||||
|
});
|
||||||
|
|
@ -19,7 +19,7 @@ export class AsyncQueue<T> {
|
||||||
|
|
||||||
push(item: T): void {
|
push(item: T): void {
|
||||||
const waiter = this.waiters.shift();
|
const waiter = this.waiters.shift();
|
||||||
if (waiter) {
|
if (waiter !== undefined) {
|
||||||
waiter(item);
|
waiter(item);
|
||||||
} else {
|
} else {
|
||||||
this.buffer.push(item);
|
this.buffer.push(item);
|
||||||
|
|
@ -34,7 +34,7 @@ export class AsyncQueue<T> {
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
const waiter = (value: T) => {
|
const waiter = (value: T) => {
|
||||||
if (timer) clearTimeout(timer);
|
if (timer !== undefined) clearTimeout(timer);
|
||||||
resolve(value);
|
resolve(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -58,17 +58,20 @@ export class AsyncQueue<T> {
|
||||||
export class Subscriber<T> {
|
export class Subscriber<T> {
|
||||||
private backend: BackendConsumer<T> | null = null;
|
private backend: BackendConsumer<T> | null = null;
|
||||||
private running = false;
|
private running = false;
|
||||||
|
private readonly pubsub: PubSubBackend;
|
||||||
|
private readonly topic: string;
|
||||||
|
private readonly subscription: string;
|
||||||
|
|
||||||
// ID-specific subscriptions (request/response correlation)
|
// ID-specific subscriptions (request/response correlation)
|
||||||
private idSubscribers = new Map<string, Resolver<T>>();
|
private idSubscribers = new Map<string, Resolver<T>>();
|
||||||
// Wildcard subscribers (receive all messages)
|
// Wildcard subscribers (receive all messages)
|
||||||
private allSubscribers = new Map<string, Resolver<T>>();
|
private allSubscribers = new Map<string, Resolver<T>>();
|
||||||
|
|
||||||
constructor(
|
constructor(pubsub: PubSubBackend, topic: string, subscription: string) {
|
||||||
private readonly pubsub: PubSubBackend,
|
this.pubsub = pubsub;
|
||||||
private readonly topic: string,
|
this.topic = topic;
|
||||||
private readonly subscription: string,
|
this.subscription = subscription;
|
||||||
) {}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
this.backend = await this.pubsub.createConsumer<T>({
|
this.backend = await this.pubsub.createConsumer<T>({
|
||||||
|
|
@ -78,13 +81,13 @@ export class Subscriber<T> {
|
||||||
this.running = true;
|
this.running = true;
|
||||||
// Start the dispatch loop (fire and forget — runs until stop)
|
// Start the dispatch loop (fire and forget — runs until stop)
|
||||||
this.dispatchLoop().catch((err) => {
|
this.dispatchLoop().catch((err) => {
|
||||||
if (this.running) console.error("[Subscriber] dispatch loop error:", err);
|
if (this.running === true) console.error("[Subscriber] dispatch loop error:", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
if (this.backend) {
|
if (this.backend !== null) {
|
||||||
await this.backend.close();
|
await this.backend.close();
|
||||||
this.backend = null;
|
this.backend = null;
|
||||||
}
|
}
|
||||||
|
|
@ -114,8 +117,11 @@ export class Subscriber<T> {
|
||||||
let consecutiveErrors = 0;
|
let consecutiveErrors = 0;
|
||||||
while (this.running) {
|
while (this.running) {
|
||||||
try {
|
try {
|
||||||
const msg = await this.backend!.receive(2000);
|
const backend = this.backend;
|
||||||
if (!msg) continue;
|
if (backend === null) throw new Error("Subscriber backend not started");
|
||||||
|
|
||||||
|
const msg = await backend.receive(2000);
|
||||||
|
if (msg === null) continue;
|
||||||
|
|
||||||
consecutiveErrors = 0;
|
consecutiveErrors = 0;
|
||||||
|
|
||||||
|
|
@ -124,9 +130,9 @@ export class Subscriber<T> {
|
||||||
const value = msg.value();
|
const value = msg.value();
|
||||||
|
|
||||||
// Route to ID-specific subscriber
|
// Route to ID-specific subscriber
|
||||||
if (id) {
|
if (id !== undefined && id.length > 0) {
|
||||||
const sub = this.idSubscribers.get(id);
|
const sub = this.idSubscribers.get(id);
|
||||||
if (sub) {
|
if (sub !== undefined) {
|
||||||
sub.queue.push(value);
|
sub.queue.push(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +142,7 @@ export class Subscriber<T> {
|
||||||
sub.queue.push(value);
|
sub.queue.push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.backend!.acknowledge(msg);
|
await backend.acknowledge(msg);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!this.running) break;
|
if (!this.running) break;
|
||||||
consecutiveErrors++;
|
consecutiveErrors++;
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,10 @@ export class ConsumerMetrics {
|
||||||
private requestHistogram: Histogram;
|
private requestHistogram: Histogram;
|
||||||
private processingCounter: Counter;
|
private processingCounter: Counter;
|
||||||
private rateLimitCounter: Counter;
|
private rateLimitCounter: Counter;
|
||||||
|
private readonly labels: { processor: string; flow: string; name: string };
|
||||||
|
|
||||||
constructor(processor: string, flow: string, name: string) {
|
constructor(processor: string, flow: string, name: string) {
|
||||||
|
this.labels = { processor, flow, name };
|
||||||
this.requestHistogram = new Histogram({
|
this.requestHistogram = new Histogram({
|
||||||
name: "tg_consumer_request_duration_seconds",
|
name: "tg_consumer_request_duration_seconds",
|
||||||
help: "Consumer request processing time",
|
help: "Consumer request processing time",
|
||||||
|
|
@ -38,22 +40,24 @@ export class ConsumerMetrics {
|
||||||
}
|
}
|
||||||
|
|
||||||
recordTime(seconds: number): void {
|
recordTime(seconds: number): void {
|
||||||
this.requestHistogram.observe(seconds);
|
this.requestHistogram.observe(this.labels, seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
process(status: "success" | "error"): void {
|
process(status: "success" | "error"): void {
|
||||||
this.processingCounter.inc({ status });
|
this.processingCounter.inc({ ...this.labels, status });
|
||||||
}
|
}
|
||||||
|
|
||||||
rateLimit(): void {
|
rateLimit(): void {
|
||||||
this.rateLimitCounter.inc();
|
this.rateLimitCounter.inc(this.labels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProducerMetrics {
|
export class ProducerMetrics {
|
||||||
private counter: Counter;
|
private counter: Counter;
|
||||||
|
private readonly labels: { processor: string; flow: string; name: string };
|
||||||
|
|
||||||
constructor(processor: string, flow: string, name: string) {
|
constructor(processor: string, flow: string, name: string) {
|
||||||
|
this.labels = { processor, flow, name };
|
||||||
this.counter = new Counter({
|
this.counter = new Counter({
|
||||||
name: "tg_producer_items_total",
|
name: "tg_producer_items_total",
|
||||||
help: "Producer items sent",
|
help: "Producer items sent",
|
||||||
|
|
@ -63,6 +67,6 @@ export class ProducerMetrics {
|
||||||
}
|
}
|
||||||
|
|
||||||
inc(): void {
|
inc(): void {
|
||||||
this.counter.inc();
|
this.counter.inc(this.labels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,16 @@
|
||||||
|
|
||||||
import type { PubSubBackend } from "../backend/types.js";
|
import type { PubSubBackend } from "../backend/types.js";
|
||||||
import { NatsBackend } from "../backend/nats.js";
|
import { NatsBackend } from "../backend/nats.js";
|
||||||
import { topics } from "../schema/topics.js";
|
import { Effect } from "effect";
|
||||||
|
import { processorLifecycleError, type ProcessorLifecycleError } from "../errors.js";
|
||||||
|
import { loadProcessorRuntimeConfig } from "../runtime/config.js";
|
||||||
|
|
||||||
export interface ProcessorConfig {
|
export interface ProcessorConfig {
|
||||||
id: string;
|
id: string;
|
||||||
pubsubUrl?: string;
|
pubsubUrl?: string;
|
||||||
metricsPort?: number;
|
metricsPort?: number;
|
||||||
|
manageProcessSignals?: boolean;
|
||||||
|
pubsub?: PubSubBackend;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigHandler = (
|
export type ConfigHandler = (
|
||||||
|
|
@ -21,14 +25,29 @@ export type ConfigHandler = (
|
||||||
version: number,
|
version: number,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
export abstract class AsyncProcessor {
|
export type EffectConfigHandler<E = never, R = never> = (
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
version: number,
|
||||||
|
) => Effect.Effect<void, E, R>;
|
||||||
|
|
||||||
|
interface RegisteredSignalHandler {
|
||||||
|
readonly signal: NodeJS.Signals;
|
||||||
|
readonly handler: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class AsyncProcessor<RunError = ProcessorLifecycleError, RunRequirements = never> {
|
||||||
protected pubsub: PubSubBackend;
|
protected pubsub: PubSubBackend;
|
||||||
protected running = false;
|
protected running = false;
|
||||||
protected configHandlers: ConfigHandler[] = [];
|
protected configHandlers: ConfigHandler[] = [];
|
||||||
private shutdownCallbacks: Array<() => Promise<void>> = [];
|
private shutdownCallbacks: Array<() => Promise<void>> = [];
|
||||||
|
private signalHandlers: RegisteredSignalHandler[] = [];
|
||||||
|
private readonly ownsPubSub: boolean;
|
||||||
|
protected readonly config: ProcessorConfig;
|
||||||
|
|
||||||
constructor(protected readonly config: ProcessorConfig) {
|
constructor(config: ProcessorConfig) {
|
||||||
this.pubsub = new NatsBackend(config.pubsubUrl ?? "nats://localhost:4222");
|
this.config = config;
|
||||||
|
this.pubsub = config.pubsub ?? new NatsBackend(config.pubsubUrl ?? "nats://localhost:4222");
|
||||||
|
this.ownsPubSub = config.pubsub === undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerConfigHandler(handler: ConfigHandler): void {
|
registerConfigHandler(handler: ConfigHandler): void {
|
||||||
|
|
@ -36,47 +55,107 @@ export abstract class AsyncProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
this.running = true;
|
await Effect.runPromise(
|
||||||
// Set up graceful shutdown
|
this.startEffect() as Effect.Effect<void, RunError | ProcessorLifecycleError>,
|
||||||
const shutdown = async () => {
|
);
|
||||||
console.log(`[${this.config.id}] Shutting down...`);
|
|
||||||
await this.stop();
|
|
||||||
process.exit(0);
|
|
||||||
};
|
|
||||||
process.on("SIGINT", shutdown);
|
|
||||||
process.on("SIGTERM", shutdown);
|
|
||||||
|
|
||||||
await this.run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
this.running = false;
|
await Effect.runPromise(this.stopEffect());
|
||||||
for (const cb of this.shutdownCallbacks) {
|
|
||||||
await cb();
|
|
||||||
}
|
|
||||||
await this.pubsub.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onShutdown(callback: () => Promise<void>): void {
|
protected onShutdown(callback: () => Promise<void>): void {
|
||||||
this.shutdownCallbacks.push(callback);
|
this.shutdownCallbacks.push(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract run(): Promise<void>;
|
startEffect(): Effect.Effect<void, RunError | ProcessorLifecycleError, RunRequirements> {
|
||||||
|
const processor = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* Effect.sync(() => {
|
||||||
|
processor.running = true;
|
||||||
|
processor.registerProcessSignalHandlers();
|
||||||
|
});
|
||||||
|
|
||||||
|
yield* processor.runEffect();
|
||||||
|
}).pipe(
|
||||||
|
Effect.withSpan("trustgraph.processor.start", {
|
||||||
|
attributes: {
|
||||||
|
"trustgraph.processor.id": processor.config.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopEffect(): Effect.Effect<void, ProcessorLifecycleError> {
|
||||||
|
const processor = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* Effect.sync(() => {
|
||||||
|
processor.running = false;
|
||||||
|
processor.unregisterProcessSignalHandlers();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const cb of processor.shutdownCallbacks) {
|
||||||
|
yield* Effect.tryPromise({
|
||||||
|
try: () => cb(),
|
||||||
|
catch: (error) => processorLifecycleError(processor.config.id, "shutdown-callback", error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processor.ownsPubSub) {
|
||||||
|
yield* Effect.tryPromise({
|
||||||
|
try: () => processor.pubsub.close(),
|
||||||
|
catch: (error) => processorLifecycleError(processor.config.id, "close-pubsub", error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected run(): Promise<void> {
|
||||||
|
return Effect.runPromise(this.runEffect() as unknown as Effect.Effect<void, RunError>);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected runEffect(): Effect.Effect<void, RunError, RunRequirements> {
|
||||||
|
return Effect.tryPromise({
|
||||||
|
try: () => this.run(),
|
||||||
|
catch: (error) => processorLifecycleError(this.config.id, "start", error),
|
||||||
|
}) as unknown as Effect.Effect<void, RunError, RunRequirements>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerProcessSignalHandlers(): void {
|
||||||
|
if (this.config.manageProcessSignals === false || this.signalHandlers.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shutdown = () => {
|
||||||
|
console.log(`[${this.config.id}] Shutting down...`);
|
||||||
|
void this.stop().then(() => process.exit(0));
|
||||||
|
};
|
||||||
|
const handlers: RegisteredSignalHandler[] = [
|
||||||
|
{ signal: "SIGINT", handler: shutdown },
|
||||||
|
{ signal: "SIGTERM", handler: shutdown },
|
||||||
|
];
|
||||||
|
for (const { signal, handler } of handlers) {
|
||||||
|
process.once(signal, handler);
|
||||||
|
}
|
||||||
|
this.signalHandlers = handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unregisterProcessSignalHandlers(): void {
|
||||||
|
for (const { signal, handler } of this.signalHandlers) {
|
||||||
|
process.off(signal, handler);
|
||||||
|
}
|
||||||
|
this.signalHandlers = [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static launch helper — parses env/args and starts the processor.
|
* Static launch helper — parses env/args and starts the processor.
|
||||||
* Subclasses call: `MyProcessor.launch("my-service")`
|
* Subclasses call: `MyProcessor.launch("my-service")`
|
||||||
*/
|
*/
|
||||||
static async launch<T extends AsyncProcessor>(
|
static async launch<T extends AsyncProcessor<unknown, unknown>>(
|
||||||
this: new (config: ProcessorConfig) => T,
|
this: new (config: ProcessorConfig) => T,
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const config: ProcessorConfig = {
|
const config = await Effect.runPromise(loadProcessorRuntimeConfig(id));
|
||||||
id,
|
|
||||||
pubsubUrl: process.env.NATS_URL ?? process.env.PULSAR_HOST,
|
|
||||||
metricsPort: parseInt(process.env.METRICS_PORT ?? "8000", 10),
|
|
||||||
};
|
|
||||||
|
|
||||||
const processor = new this(config);
|
const processor = new this(config);
|
||||||
await processor.start();
|
await processor.start();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,53 @@ import type { Spec } from "../spec/types.js";
|
||||||
import type { BackendConsumer } from "../backend/types.js";
|
import type { BackendConsumer } from "../backend/types.js";
|
||||||
import { Flow, type FlowDefinition } from "./flow.js";
|
import { Flow, type FlowDefinition } from "./flow.js";
|
||||||
import { topics } from "../schema/topics.js";
|
import { topics } from "../schema/topics.js";
|
||||||
|
import {
|
||||||
|
pubSubError,
|
||||||
|
type FlowRuntimeError,
|
||||||
|
type ProcessorLifecycleError,
|
||||||
|
type PubSubError,
|
||||||
|
} from "../errors.js";
|
||||||
|
import {
|
||||||
|
ConsumerFactory,
|
||||||
|
FlowRuntime,
|
||||||
|
ProducerFactory,
|
||||||
|
RequestResponseFactory,
|
||||||
|
makeConsumerFactoryService,
|
||||||
|
makeProducerFactoryService,
|
||||||
|
makeRequestResponseFactoryService,
|
||||||
|
runFlowRuntimeScoped,
|
||||||
|
} from "../messaging/runtime.js";
|
||||||
|
import { makePubSubService, PubSub } from "../backend/pubsub.js";
|
||||||
|
import { loadMessagingRuntimeConfig } from "../runtime/messaging-config.js";
|
||||||
|
import { Duration, Effect, Exit, Scope } from "effect";
|
||||||
|
import * as S from "effect/Schema";
|
||||||
|
|
||||||
interface ConfigPush {
|
interface ConfigPush {
|
||||||
version: number;
|
version: number;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class FlowProcessor extends AsyncProcessor {
|
interface ActiveFlow {
|
||||||
private specifications: Spec[] = [];
|
readonly scope: Scope.Closeable;
|
||||||
private flows = new Map<string, Flow>();
|
}
|
||||||
|
|
||||||
|
const ConfigPushSchema = S.Struct({
|
||||||
|
version: S.Number,
|
||||||
|
config: S.Record(S.String, S.Unknown),
|
||||||
|
});
|
||||||
|
|
||||||
|
export abstract class FlowProcessor<FlowRequirements = never> extends AsyncProcessor<
|
||||||
|
PubSubError | FlowRuntimeError | ProcessorLifecycleError,
|
||||||
|
| PubSub
|
||||||
|
| FlowRuntime
|
||||||
|
| ProducerFactory
|
||||||
|
| ConsumerFactory
|
||||||
|
| RequestResponseFactory
|
||||||
|
| Scope.Scope
|
||||||
|
| FlowRequirements
|
||||||
|
> {
|
||||||
|
private specifications: Array<Spec<FlowRequirements>> = [];
|
||||||
|
private flows = new Map<string, ActiveFlow>();
|
||||||
private configConsumer: BackendConsumer<ConfigPush> | null = null;
|
private configConsumer: BackendConsumer<ConfigPush> | null = null;
|
||||||
private lastFlowsJson = "";
|
private lastFlowsJson = "";
|
||||||
|
|
||||||
|
|
@ -28,110 +66,254 @@ export abstract class FlowProcessor extends AsyncProcessor {
|
||||||
super(config);
|
super(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSpecification(spec: Spec): void {
|
registerSpecification<Requirements extends FlowRequirements>(
|
||||||
this.specifications.push(spec);
|
spec: Spec<Requirements>,
|
||||||
|
): void {
|
||||||
|
this.specifications.push(spec as Spec<FlowRequirements>);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async run(): Promise<void> {
|
override async start(): Promise<void> {
|
||||||
// Subscribe to config-push topic to receive flow definitions.
|
const pubsub = makePubSubService(this.pubsub);
|
||||||
// Use "earliest" to replay any config pushes that arrived before this service started.
|
const messagingConfig = await Effect.runPromise(loadMessagingRuntimeConfig());
|
||||||
this.configConsumer = await this.pubsub.createConsumer<ConfigPush>({
|
const start = this.startEffect().pipe(
|
||||||
topic: topics.configPush,
|
Effect.provideService(PubSub, pubsub),
|
||||||
subscription: `${this.config.id}-config-push`,
|
Effect.provideService(ProducerFactory, ProducerFactory.of(makeProducerFactoryService(pubsub))),
|
||||||
initialPosition: "earliest",
|
Effect.provideService(ConsumerFactory, ConsumerFactory.of(makeConsumerFactoryService(pubsub, messagingConfig))),
|
||||||
|
Effect.provideService(
|
||||||
|
RequestResponseFactory,
|
||||||
|
RequestResponseFactory.of(makeRequestResponseFactoryService(pubsub, messagingConfig)),
|
||||||
|
),
|
||||||
|
Effect.provideService(FlowRuntime, FlowRuntime.of({ run: runFlowRuntimeScoped })),
|
||||||
|
) as Effect.Effect<void, PubSubError | FlowRuntimeError | ProcessorLifecycleError>;
|
||||||
|
await Effect.runPromise(
|
||||||
|
Effect.scoped(
|
||||||
|
start,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override runEffect(): Effect.Effect<
|
||||||
|
void,
|
||||||
|
PubSubError | FlowRuntimeError | ProcessorLifecycleError,
|
||||||
|
| PubSub
|
||||||
|
| FlowRuntime
|
||||||
|
| ProducerFactory
|
||||||
|
| ConsumerFactory
|
||||||
|
| RequestResponseFactory
|
||||||
|
| Scope.Scope
|
||||||
|
| FlowRequirements
|
||||||
|
> {
|
||||||
|
const processor = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
|
||||||
|
// Subscribe to config-push topic to receive flow definitions.
|
||||||
|
// Use "earliest" to replay any config pushes that arrived before this service started.
|
||||||
|
processor.configConsumer = yield* pubsub.createConsumer<ConfigPush>({
|
||||||
|
topic: topics.configPush,
|
||||||
|
subscription: `${processor.config.id}-config-push`,
|
||||||
|
initialPosition: "earliest",
|
||||||
|
schema: ConfigPushSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
processor.closeConfigConsumerEffect().pipe(
|
||||||
|
Effect.flatMap(() => processor.closeAllFlowsEffect()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
yield* Effect.log(`[${processor.config.id}] Listening for config pushes on ${topics.configPush}`);
|
||||||
|
|
||||||
|
yield* Effect.whileLoop({
|
||||||
|
while: () => processor.running,
|
||||||
|
body: () => processor.processNextConfigPushEffect(),
|
||||||
|
step: () => undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[${this.config.id}] Listening for config pushes on ${topics.configPush}`);
|
private onConfigureFlowsEffect(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
_version: number,
|
||||||
|
): Effect.Effect<
|
||||||
|
void,
|
||||||
|
FlowRuntimeError,
|
||||||
|
FlowRuntime | ProducerFactory | ConsumerFactory | RequestResponseFactory | FlowRequirements
|
||||||
|
> {
|
||||||
|
const processor = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const flowDefs = config.flows as Record<string, FlowDefinition> | undefined;
|
||||||
|
if (flowDefs === undefined) {
|
||||||
|
yield* Effect.log(`[${processor.config.id}] No flows in config push, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
while (this.running) {
|
// Skip flow restart if the flow definitions haven't changed.
|
||||||
try {
|
// This prevents disrupting in-flight requests when non-flow config
|
||||||
const msg = await this.configConsumer.receive(2000);
|
// sections (prompts, tools, mcp) are updated.
|
||||||
if (!msg) continue;
|
const flowsJson = yield* S.encodeUnknownEffect(S.UnknownFromJsonString)(flowDefs).pipe(
|
||||||
|
Effect.catch((error) => Effect.succeed(String(error))),
|
||||||
|
);
|
||||||
|
if (processor.lastFlowsJson.length > 0 && flowsJson === processor.lastFlowsJson && processor.flows.size > 0) {
|
||||||
|
yield* Effect.log(`[${processor.config.id}] Flow definitions unchanged, skipping restart`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processor.lastFlowsJson = flowsJson;
|
||||||
|
|
||||||
const push = msg.value();
|
// Stop removed flows
|
||||||
console.log(`[${this.config.id}] Received config push version=${push.version}`);
|
for (const [name, activeFlow] of processor.flows) {
|
||||||
|
if (!(name in flowDefs)) {
|
||||||
|
yield* Effect.log(`[${processor.config.id}] Stopping removed flow: ${name}`);
|
||||||
|
yield* processor.closeFlowEffect(name, activeFlow);
|
||||||
|
processor.flows.delete(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.onConfigureFlows(push.config, push.version);
|
// Start or update flows
|
||||||
|
for (const [name, defn] of Object.entries(flowDefs)) {
|
||||||
// Also call any registered config handlers
|
// Skip invalid definitions (e.g., stringified JSON)
|
||||||
for (const handler of this.configHandlers) {
|
if (typeof defn !== "object" || defn === null) {
|
||||||
await handler(push.config, push.version);
|
yield* Effect.logWarning(`[${processor.config.id}] Skipping flow "${name}": definition is not an object`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.configConsumer.acknowledge(msg);
|
// Stop existing flow before (re)starting with new config
|
||||||
} catch (err) {
|
const existing = processor.flows.get(name);
|
||||||
if (!this.running) break;
|
if (existing !== undefined) {
|
||||||
console.error(`[${this.config.id}] Config consumer error:`, err);
|
yield* Effect.log(`[${processor.config.id}] Restarting flow "${name}" with updated config`);
|
||||||
await sleep(1000);
|
yield* processor.closeFlowEffect(name, existing);
|
||||||
|
processor.flows.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* Effect.log(`[${processor.config.id}] Starting flow "${name}"`);
|
||||||
|
const activeFlow = yield* processor.startFlowEffect(name, defn);
|
||||||
|
processor.flows.set(name, activeFlow);
|
||||||
|
yield* Effect.log(`[${processor.config.id}] Flow "${name}" started`);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onConfigureFlows(
|
override stopEffect(): Effect.Effect<void, ProcessorLifecycleError> {
|
||||||
config: Record<string, unknown>,
|
return this.closeConfigConsumerEffect().pipe(
|
||||||
version: number,
|
Effect.flatMap(() => this.closeAllFlowsEffect()),
|
||||||
): Promise<void> {
|
Effect.flatMap(() => super.stopEffect()),
|
||||||
const flowDefs = config.flows as Record<string, FlowDefinition> | undefined;
|
);
|
||||||
if (!flowDefs) {
|
|
||||||
console.log(`[${this.config.id}] No flows in config push, skipping`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip flow restart if the flow definitions haven't changed.
|
|
||||||
// This prevents disrupting in-flight requests when non-flow config
|
|
||||||
// sections (prompts, tools, mcp) are updated.
|
|
||||||
const flowsJson = JSON.stringify(flowDefs);
|
|
||||||
if (this.lastFlowsJson && flowsJson === this.lastFlowsJson && this.flows.size > 0) {
|
|
||||||
console.log(`[${this.config.id}] Flow definitions unchanged, skipping restart`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.lastFlowsJson = flowsJson;
|
|
||||||
|
|
||||||
// Stop removed flows
|
|
||||||
for (const [name, flow] of this.flows) {
|
|
||||||
if (!(name in flowDefs)) {
|
|
||||||
console.log(`[${this.config.id}] Stopping removed flow: ${name}`);
|
|
||||||
await flow.stop();
|
|
||||||
this.flows.delete(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start or update flows
|
|
||||||
for (const [name, defn] of Object.entries(flowDefs)) {
|
|
||||||
// Skip invalid definitions (e.g., stringified JSON)
|
|
||||||
if (typeof defn !== "object" || defn === null) {
|
|
||||||
console.warn(`[${this.config.id}] Skipping flow "${name}": definition is not an object`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop existing flow before (re)starting with new config
|
|
||||||
if (this.flows.has(name)) {
|
|
||||||
console.log(`[${this.config.id}] Restarting flow "${name}" with updated config`);
|
|
||||||
await this.flows.get(name)!.stop();
|
|
||||||
this.flows.delete(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[${this.config.id}] Starting flow "${name}" with topics:`, defn.topics);
|
|
||||||
const flow = new Flow(name, this.config.id, this.pubsub, defn, this.specifications);
|
|
||||||
await flow.start();
|
|
||||||
this.flows.set(name, flow);
|
|
||||||
console.log(`[${this.config.id}] Flow "${name}" started`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override async stop(): Promise<void> {
|
private processNextConfigPushEffect(): Effect.Effect<
|
||||||
if (this.configConsumer) {
|
void,
|
||||||
await this.configConsumer.close();
|
never,
|
||||||
this.configConsumer = null;
|
FlowRuntime | ProducerFactory | ConsumerFactory | RequestResponseFactory | FlowRequirements
|
||||||
|
> {
|
||||||
|
const processor = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const consumer = processor.configConsumer;
|
||||||
|
if (consumer === null) {
|
||||||
|
yield* Effect.sleep(Duration.millis(1000));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = yield* Effect.tryPromise({
|
||||||
|
try: () => consumer.receive(2000),
|
||||||
|
catch: (error) => pubSubError("receive:config-push", error),
|
||||||
|
});
|
||||||
|
if (msg === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const push = msg.value();
|
||||||
|
yield* Effect.log(`[${processor.config.id}] Received config push version=${push.version}`);
|
||||||
|
|
||||||
|
yield* processor.onConfigureFlowsEffect(push.config, push.version);
|
||||||
|
|
||||||
|
for (const handler of processor.configHandlers) {
|
||||||
|
yield* Effect.tryPromise({
|
||||||
|
try: () => handler(push.config, push.version),
|
||||||
|
catch: (error) => pubSubError("config-handler", error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* Effect.tryPromise({
|
||||||
|
try: () => consumer.acknowledge(msg),
|
||||||
|
catch: (error) => pubSubError("acknowledge:config-push", error),
|
||||||
|
});
|
||||||
|
}).pipe(
|
||||||
|
Effect.catch((error) => {
|
||||||
|
if (!processor.running) {
|
||||||
|
return Effect.void;
|
||||||
|
}
|
||||||
|
return Effect.logError(`[${processor.config.id}] Config consumer error`, {
|
||||||
|
error: error.message,
|
||||||
|
}).pipe(
|
||||||
|
Effect.flatMap(() => Effect.sleep(Duration.millis(1000))),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startFlowEffect(
|
||||||
|
name: string,
|
||||||
|
definition: FlowDefinition,
|
||||||
|
): Effect.Effect<
|
||||||
|
ActiveFlow,
|
||||||
|
FlowRuntimeError,
|
||||||
|
FlowRuntime | ProducerFactory | ConsumerFactory | RequestResponseFactory | FlowRequirements
|
||||||
|
> {
|
||||||
|
const processor = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const flowRuntime = yield* FlowRuntime;
|
||||||
|
const scope = yield* Scope.make();
|
||||||
|
const flow = new Flow<FlowRequirements>(
|
||||||
|
name,
|
||||||
|
processor.config.id,
|
||||||
|
processor.pubsub,
|
||||||
|
definition,
|
||||||
|
processor.specifications,
|
||||||
|
);
|
||||||
|
return yield* flowRuntime.run(flow).pipe(
|
||||||
|
Scope.provide(scope),
|
||||||
|
Effect.as({ scope } satisfies ActiveFlow),
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Scope.close(scope, Exit.void).pipe(
|
||||||
|
Effect.flatMap(() => Effect.fail(error)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeFlowEffect(name: string, activeFlow: ActiveFlow): Effect.Effect<void> {
|
||||||
|
return Scope.close(activeFlow.scope, Exit.void).pipe(
|
||||||
|
Effect.tap(() => Effect.log(`[${this.config.id}] Flow "${name}" stopped`)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeAllFlowsEffect(): Effect.Effect<void> {
|
||||||
|
const processor = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const flows = Array.from(processor.flows.entries());
|
||||||
|
for (const [name, activeFlow] of flows) {
|
||||||
|
yield* processor.closeFlowEffect(name, activeFlow);
|
||||||
|
}
|
||||||
|
processor.flows.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeConfigConsumerEffect(): Effect.Effect<void> {
|
||||||
|
const consumer = this.configConsumer;
|
||||||
|
this.configConsumer = null;
|
||||||
|
if (consumer === null) {
|
||||||
|
return Effect.void;
|
||||||
}
|
}
|
||||||
for (const flow of this.flows.values()) {
|
return Effect.tryPromise({
|
||||||
await flow.stop();
|
try: () => consumer.close(),
|
||||||
}
|
catch: (error) => pubSubError("close:config-push", error),
|
||||||
this.flows.clear();
|
}).pipe(
|
||||||
await super.stop();
|
Effect.catch((error) =>
|
||||||
|
Effect.logError(`[${this.config.id}] Failed to close config consumer`, {
|
||||||
|
error: error.message,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms: number): Promise<void> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,28 @@
|
||||||
* Python reference: trustgraph-base/trustgraph/base/flow.py
|
* Python reference: trustgraph-base/trustgraph/base/flow.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Effect, Exit, Scope } from "effect";
|
||||||
import type { PubSubBackend } from "../backend/types.js";
|
import type { PubSubBackend } from "../backend/types.js";
|
||||||
import type { Spec } from "../spec/types.js";
|
import { makePubSubService } from "../backend/pubsub.js";
|
||||||
import type { Producer } from "../messaging/producer.js";
|
import {
|
||||||
import type { Consumer } from "../messaging/consumer.js";
|
flowResourceNotFoundError,
|
||||||
import type { RequestResponse } from "../messaging/request-response.js";
|
type FlowResourceNotFoundError,
|
||||||
|
type PubSubError,
|
||||||
|
} from "../errors.js";
|
||||||
|
import {
|
||||||
|
ConsumerFactory,
|
||||||
|
ProducerFactory,
|
||||||
|
RequestResponseFactory,
|
||||||
|
type EffectConsumer,
|
||||||
|
type EffectProducer,
|
||||||
|
type EffectRequestOptions,
|
||||||
|
type EffectRequestResponse,
|
||||||
|
makeConsumerFactoryService,
|
||||||
|
makeProducerFactoryService,
|
||||||
|
makeRequestResponseFactoryService,
|
||||||
|
} from "../messaging/runtime.js";
|
||||||
|
import { loadMessagingRuntimeConfig } from "../runtime/messaging-config.js";
|
||||||
|
import type { Spec, SpecRuntimeRequirements } from "../spec/types.js";
|
||||||
|
|
||||||
export interface FlowDefinition {
|
export interface FlowDefinition {
|
||||||
/** Topic overrides keyed by spec name */
|
/** Topic overrides keyed by spec name */
|
||||||
|
|
@ -17,54 +34,119 @@ export interface FlowDefinition {
|
||||||
parameters?: Record<string, unknown>;
|
parameters?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Flow {
|
export interface FlowProducer<T> {
|
||||||
private producers = new Map<string, Producer<unknown>>();
|
readonly send: (id: string, message: T) => Promise<void>;
|
||||||
private consumers = new Map<string, Consumer<unknown>>();
|
readonly flush: () => Promise<void>;
|
||||||
private requestors = new Map<string, RequestResponse<unknown, unknown>>();
|
readonly stop: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowConsumer {
|
||||||
|
readonly stop: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowRequestOptions<TRes> {
|
||||||
|
readonly timeoutMs?: number;
|
||||||
|
readonly recipient?: (response: TRes) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowRequestor<TReq, TRes> {
|
||||||
|
readonly request: (
|
||||||
|
request: TReq,
|
||||||
|
options?: FlowRequestOptions<TRes>,
|
||||||
|
) => Promise<TRes>;
|
||||||
|
readonly stop: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Flow<Requirements = never> {
|
||||||
|
private producers = new Map<string, EffectProducer<unknown>>();
|
||||||
|
private consumers = new Map<string, EffectConsumer>();
|
||||||
|
private requestors = new Map<string, EffectRequestResponse<unknown, unknown>>();
|
||||||
private parameters = new Map<string, unknown>();
|
private parameters = new Map<string, unknown>();
|
||||||
|
private compatibilityScope: Scope.Closeable | null = null;
|
||||||
|
public readonly name: string;
|
||||||
|
public readonly processorId: string;
|
||||||
|
private readonly pubsub: PubSubBackend;
|
||||||
|
private readonly definition: FlowDefinition;
|
||||||
|
private readonly specifications: ReadonlyArray<Spec<Requirements>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
name: string,
|
||||||
public readonly processorId: string,
|
processorId: string,
|
||||||
private readonly pubsub: PubSubBackend,
|
pubsub: PubSubBackend,
|
||||||
private readonly definition: FlowDefinition,
|
definition: FlowDefinition,
|
||||||
private readonly specifications: Spec[],
|
specifications: ReadonlyArray<Spec<Requirements>>,
|
||||||
) {}
|
) {
|
||||||
|
this.name = name;
|
||||||
|
this.processorId = processorId;
|
||||||
|
this.pubsub = pubsub;
|
||||||
|
this.definition = definition;
|
||||||
|
this.specifications = specifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
startEffect(): Effect.Effect<void, PubSubError, SpecRuntimeRequirements | Requirements> {
|
||||||
|
const flow = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
for (const spec of flow.specifications) {
|
||||||
|
yield* spec.addEffect(flow, flow.definition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
for (const spec of this.specifications) {
|
if (this.compatibilityScope !== null) {
|
||||||
await spec.add(this, this.pubsub, this.definition);
|
await this.stop();
|
||||||
}
|
|
||||||
|
|
||||||
// Start all consumers, passing this Flow instance via FlowContext
|
|
||||||
for (const consumer of this.consumers.values()) {
|
|
||||||
consumer.start({ id: this.processorId, name: this.name, flow: this }).catch((err) => {
|
|
||||||
console.error(`[Flow:${this.name}] Consumer error:`, err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
await this.runInCompatibilityScope(
|
||||||
|
this.startEffect() as Effect.Effect<void, PubSubError, SpecRuntimeRequirements>,
|
||||||
|
this.pubsub,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
for (const consumer of this.consumers.values()) {
|
const scope = this.compatibilityScope;
|
||||||
await consumer.stop();
|
this.compatibilityScope = null;
|
||||||
}
|
if (scope !== null) {
|
||||||
for (const producer of this.producers.values()) {
|
await Effect.runPromise(Scope.close(scope, Exit.void));
|
||||||
await producer.stop();
|
|
||||||
}
|
|
||||||
for (const rr of this.requestors.values()) {
|
|
||||||
await rr.stop();
|
|
||||||
}
|
}
|
||||||
|
this.clearResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
registerProducer(name: string, producer: Producer<unknown>): void {
|
async runInCompatibilityScope<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, SpecRuntimeRequirements>,
|
||||||
|
pubsub: PubSubBackend,
|
||||||
|
): Promise<A> {
|
||||||
|
const scope = await this.ensureCompatibilityScope();
|
||||||
|
const pubsubService = makePubSubService(pubsub);
|
||||||
|
const messagingConfig = await Effect.runPromise(loadMessagingRuntimeConfig());
|
||||||
|
return await Effect.runPromise(
|
||||||
|
effect.pipe(
|
||||||
|
Effect.provideService(ProducerFactory, ProducerFactory.of(makeProducerFactoryService(pubsubService))),
|
||||||
|
Effect.provideService(ConsumerFactory, ConsumerFactory.of(makeConsumerFactoryService(pubsubService, messagingConfig))),
|
||||||
|
Effect.provideService(
|
||||||
|
RequestResponseFactory,
|
||||||
|
RequestResponseFactory.of(makeRequestResponseFactoryService(pubsubService, messagingConfig)),
|
||||||
|
),
|
||||||
|
Scope.provide(scope),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearResources(): void {
|
||||||
|
this.producers.clear();
|
||||||
|
this.consumers.clear();
|
||||||
|
this.requestors.clear();
|
||||||
|
this.parameters.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProducer(name: string, producer: EffectProducer<unknown>): void {
|
||||||
this.producers.set(name, producer);
|
this.producers.set(name, producer);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerConsumer(name: string, consumer: Consumer<unknown>): void {
|
registerConsumer(name: string, consumer: EffectConsumer): void {
|
||||||
this.consumers.set(name, consumer);
|
this.consumers.set(name, consumer);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRequestor(name: string, rr: RequestResponse<unknown, unknown>): void {
|
registerRequestor(name: string, rr: EffectRequestResponse<unknown, unknown>): void {
|
||||||
this.requestors.set(name, rr);
|
this.requestors.set(name, rr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,27 +154,97 @@ export class Flow {
|
||||||
this.parameters.set(name, value);
|
this.parameters.set(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
producer<T>(name: string): Producer<T> {
|
producerEffect<T>(name: string): Effect.Effect<EffectProducer<T>, FlowResourceNotFoundError> {
|
||||||
const p = this.producers.get(name);
|
const p = this.producers.get(name);
|
||||||
if (!p) throw new Error(`Producer "${name}" not found in flow "${this.name}"`);
|
return p === undefined
|
||||||
return p as Producer<T>;
|
? Effect.fail(flowResourceNotFoundError(this.name, "producer", name))
|
||||||
|
: Effect.succeed(p as EffectProducer<T>);
|
||||||
}
|
}
|
||||||
|
|
||||||
consumer<T>(name: string): Consumer<T> {
|
consumerEffect(name: string): Effect.Effect<EffectConsumer, FlowResourceNotFoundError> {
|
||||||
const c = this.consumers.get(name);
|
const c = this.consumers.get(name);
|
||||||
if (!c) throw new Error(`Consumer "${name}" not found in flow "${this.name}"`);
|
return c === undefined
|
||||||
return c as Consumer<T>;
|
? Effect.fail(flowResourceNotFoundError(this.name, "consumer", name))
|
||||||
|
: Effect.succeed(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
requestor<TReq, TRes>(name: string): RequestResponse<TReq, TRes> {
|
requestorEffect<TReq, TRes>(
|
||||||
|
name: string,
|
||||||
|
): Effect.Effect<EffectRequestResponse<TReq, TRes>, FlowResourceNotFoundError> {
|
||||||
const rr = this.requestors.get(name);
|
const rr = this.requestors.get(name);
|
||||||
if (!rr) throw new Error(`Requestor "${name}" not found in flow "${this.name}"`);
|
return rr === undefined
|
||||||
return rr as RequestResponse<TReq, TRes>;
|
? Effect.fail(flowResourceNotFoundError(this.name, "requestor", name))
|
||||||
|
: Effect.succeed(rr as EffectRequestResponse<TReq, TRes>);
|
||||||
|
}
|
||||||
|
|
||||||
|
parameterEffect<T>(name: string): Effect.Effect<T, FlowResourceNotFoundError> {
|
||||||
|
const v = this.parameters.get(name);
|
||||||
|
return v === undefined
|
||||||
|
? Effect.fail(flowResourceNotFoundError(this.name, "parameter", name))
|
||||||
|
: Effect.succeed(v as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
producer<T>(name: string): FlowProducer<T> {
|
||||||
|
const p = this.producers.get(name);
|
||||||
|
if (p === undefined) throw flowResourceNotFoundError(this.name, "producer", name);
|
||||||
|
return {
|
||||||
|
send: (id, message) => Effect.runPromise((p as EffectProducer<T>).send(id, message)),
|
||||||
|
flush: () => Effect.runPromise(p.flush),
|
||||||
|
stop: () => Effect.runPromise(p.flush.pipe(Effect.flatMap(() => p.close))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
consumer(name: string): FlowConsumer {
|
||||||
|
const c = this.consumers.get(name);
|
||||||
|
if (c === undefined) throw flowResourceNotFoundError(this.name, "consumer", name);
|
||||||
|
return {
|
||||||
|
stop: () => Effect.runPromise(c.stop),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
requestor<TReq, TRes>(name: string): FlowRequestor<TReq, TRes> {
|
||||||
|
const rr = this.requestors.get(name);
|
||||||
|
if (rr === undefined) throw flowResourceNotFoundError(this.name, "requestor", name);
|
||||||
|
return {
|
||||||
|
request: (request, options) =>
|
||||||
|
Effect.runPromise(
|
||||||
|
(rr as EffectRequestResponse<TReq, TRes>).request(
|
||||||
|
request,
|
||||||
|
this.toEffectRequestOptions(options),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
stop: () => Effect.runPromise(rr.stop),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
parameter<T>(name: string): T {
|
parameter<T>(name: string): T {
|
||||||
const v = this.parameters.get(name);
|
const v = this.parameters.get(name);
|
||||||
if (v === undefined) throw new Error(`Parameter "${name}" not found in flow "${this.name}"`);
|
if (v === undefined) throw flowResourceNotFoundError(this.name, "parameter", name);
|
||||||
return v as T;
|
return v as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureCompatibilityScope(): Promise<Scope.Closeable> {
|
||||||
|
if (this.compatibilityScope !== null) {
|
||||||
|
return this.compatibilityScope;
|
||||||
|
}
|
||||||
|
this.compatibilityScope = await Effect.runPromise(Scope.make());
|
||||||
|
return this.compatibilityScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toEffectRequestOptions<TRes>(
|
||||||
|
options: FlowRequestOptions<TRes> | undefined,
|
||||||
|
): EffectRequestOptions<TRes> | undefined {
|
||||||
|
if (options === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const recipient = options.recipient;
|
||||||
|
return {
|
||||||
|
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
||||||
|
...(recipient === undefined
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
recipient: (response: TRes) => Effect.promise(() => recipient(response)),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,22 @@
|
||||||
export { AsyncProcessor, type ProcessorConfig, type ConfigHandler } from "./async-processor.js";
|
export {
|
||||||
|
AsyncProcessor,
|
||||||
|
type ConfigHandler,
|
||||||
|
type EffectConfigHandler,
|
||||||
|
type ProcessorConfig,
|
||||||
|
} from "./async-processor.js";
|
||||||
export { FlowProcessor } from "./flow-processor.js";
|
export { FlowProcessor } from "./flow-processor.js";
|
||||||
export { Flow, type FlowDefinition } from "./flow.js";
|
export {
|
||||||
|
Flow,
|
||||||
|
type FlowConsumer,
|
||||||
|
type FlowDefinition,
|
||||||
|
type FlowProducer,
|
||||||
|
type FlowRequestOptions,
|
||||||
|
type FlowRequestor,
|
||||||
|
} from "./flow.js";
|
||||||
|
export {
|
||||||
|
makeAsyncProcessorProgram,
|
||||||
|
makeFlowProcessorProgram,
|
||||||
|
makeProcessorProgram,
|
||||||
|
runProcessorScoped,
|
||||||
|
type ProcessorProgramOptions,
|
||||||
|
} from "./program.js";
|
||||||
|
|
|
||||||
139
ts/packages/base/src/processor/program.ts
Normal file
139
ts/packages/base/src/processor/program.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
/**
|
||||||
|
* Scoped Effect runtime helpers for legacy processor classes.
|
||||||
|
*
|
||||||
|
* These helpers make `Context.Service`/Layer composition the canonical
|
||||||
|
* executable path while the processor internals remain Promise-based.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Effect, Scope } from "effect";
|
||||||
|
import { processorLifecycleError, type ProcessorLifecycleError } from "../errors.js";
|
||||||
|
import { NatsBackend } from "../backend/nats.js";
|
||||||
|
import { makePubSubService, PubSub } from "../backend/pubsub.js";
|
||||||
|
import {
|
||||||
|
ConsumerFactory,
|
||||||
|
FlowRuntime,
|
||||||
|
ProducerFactory,
|
||||||
|
RequestResponseFactory,
|
||||||
|
makeConsumerFactoryService,
|
||||||
|
makeProducerFactoryService,
|
||||||
|
makeRequestResponseFactoryService,
|
||||||
|
runFlowRuntimeScoped,
|
||||||
|
} from "../messaging/runtime.js";
|
||||||
|
import {
|
||||||
|
loadProcessorRuntimeConfig,
|
||||||
|
type ProcessorRuntimeConfigOptions,
|
||||||
|
} from "../runtime/config.js";
|
||||||
|
import { loadMessagingRuntimeConfig } from "../runtime/messaging-config.js";
|
||||||
|
import type { AsyncProcessor, ProcessorConfig } from "./async-processor.js";
|
||||||
|
|
||||||
|
type ProcessorRunError<Processor> = Processor extends AsyncProcessor<infer Error, unknown> ? Error : never;
|
||||||
|
type ProcessorRunRequirements<Processor> = Processor extends AsyncProcessor<unknown, infer Requirements> ? Requirements : never;
|
||||||
|
|
||||||
|
export interface ProcessorProgramOptions<
|
||||||
|
Config extends ProcessorConfig,
|
||||||
|
Error,
|
||||||
|
Requirements,
|
||||||
|
Processor extends AsyncProcessor<unknown, unknown>,
|
||||||
|
> {
|
||||||
|
readonly id: string;
|
||||||
|
readonly make: (config: Config) => Processor;
|
||||||
|
readonly loadConfig?: Effect.Effect<Config, Error, Requirements>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runProcessorScoped<
|
||||||
|
Config extends ProcessorConfig,
|
||||||
|
Processor extends AsyncProcessor<unknown, unknown>,
|
||||||
|
>(
|
||||||
|
config: Config,
|
||||||
|
make: (config: Config) => Processor,
|
||||||
|
): Effect.Effect<
|
||||||
|
void,
|
||||||
|
ProcessorRunError<Processor> | ProcessorLifecycleError,
|
||||||
|
PubSub | Scope.Scope | ProcessorRunRequirements<Processor>
|
||||||
|
> {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const pubsub = yield* PubSub;
|
||||||
|
const runtimeConfig = {
|
||||||
|
...config,
|
||||||
|
manageProcessSignals: false,
|
||||||
|
pubsub: pubsub.backend,
|
||||||
|
} as Config;
|
||||||
|
const processor = make(runtimeConfig);
|
||||||
|
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => processor.stop(),
|
||||||
|
catch: (error) => processorLifecycleError(config.id, "stop", error),
|
||||||
|
}).pipe(
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.logError("[Processor] Failed to stop processor", {
|
||||||
|
error: error.message,
|
||||||
|
operation: error.operation,
|
||||||
|
processorId: error.processorId,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const typedProcessor = processor as unknown as AsyncProcessor<
|
||||||
|
ProcessorRunError<Processor>,
|
||||||
|
ProcessorRunRequirements<Processor>
|
||||||
|
>;
|
||||||
|
yield* typedProcessor.startEffect();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeProcessorProgram<
|
||||||
|
Config extends ProcessorConfig,
|
||||||
|
Error = never,
|
||||||
|
Requirements = never,
|
||||||
|
Processor extends AsyncProcessor<unknown, unknown> = AsyncProcessor,
|
||||||
|
>(
|
||||||
|
options: ProcessorProgramOptions<Config, Error, Requirements, Processor>,
|
||||||
|
) {
|
||||||
|
return Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const config = yield* (
|
||||||
|
options.loadConfig ??
|
||||||
|
loadProcessorRuntimeConfig(options.id, {
|
||||||
|
manageProcessSignals: false,
|
||||||
|
} satisfies ProcessorRuntimeConfigOptions)
|
||||||
|
);
|
||||||
|
|
||||||
|
const runtimeConfig = {
|
||||||
|
...config,
|
||||||
|
manageProcessSignals: false,
|
||||||
|
} as Config;
|
||||||
|
|
||||||
|
const pubsub = makePubSubService(new NatsBackend(runtimeConfig.pubsubUrl ?? "nats://localhost:4222"));
|
||||||
|
const messagingConfig = yield* loadMessagingRuntimeConfig();
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
pubsub.close.pipe(
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.logError("[PubSub] Failed to close processor backend", {
|
||||||
|
error: error.message,
|
||||||
|
operation: error.operation,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const processorEffect = runProcessorScoped<Config, Processor>(
|
||||||
|
runtimeConfig,
|
||||||
|
options.make,
|
||||||
|
);
|
||||||
|
yield* processorEffect.pipe(
|
||||||
|
Effect.provideService(PubSub, pubsub),
|
||||||
|
Effect.provideService(ProducerFactory, ProducerFactory.of(makeProducerFactoryService(pubsub))),
|
||||||
|
Effect.provideService(ConsumerFactory, ConsumerFactory.of(makeConsumerFactoryService(pubsub, messagingConfig))),
|
||||||
|
Effect.provideService(
|
||||||
|
RequestResponseFactory,
|
||||||
|
RequestResponseFactory.of(makeRequestResponseFactoryService(pubsub, messagingConfig)),
|
||||||
|
),
|
||||||
|
Effect.provideService(FlowRuntime, FlowRuntime.of({ run: runFlowRuntimeScoped })),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeAsyncProcessorProgram = makeProcessorProgram;
|
||||||
|
export const makeFlowProcessorProgram = makeProcessorProgram;
|
||||||
33
ts/packages/base/src/runtime/config.ts
Normal file
33
ts/packages/base/src/runtime/config.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Effect Config contracts for process/runtime settings.
|
||||||
|
*
|
||||||
|
* These declarations preserve the existing environment variable names while
|
||||||
|
* moving reads to a typed Effect boundary.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Config, Effect } from "effect";
|
||||||
|
import * as O from "effect/Option";
|
||||||
|
|
||||||
|
export interface ProcessorRuntimeConfigOptions {
|
||||||
|
readonly manageProcessSignals?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const optionalStringConfig = Effect.fn("optionalStringConfig")(function* (name: string) {
|
||||||
|
return O.getOrUndefined(yield* Config.string(name).pipe(Config.option));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loadProcessorRuntimeConfig = Effect.fn("loadProcessorRuntimeConfig")(function* (
|
||||||
|
id: string,
|
||||||
|
options: ProcessorRuntimeConfigOptions = {},
|
||||||
|
) {
|
||||||
|
const natsUrl = yield* optionalStringConfig("NATS_URL");
|
||||||
|
const pulsarHost = yield* optionalStringConfig("PULSAR_HOST");
|
||||||
|
const metricsPort = yield* Config.number("METRICS_PORT").pipe(Config.withDefault(8000));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
pubsubUrl: natsUrl ?? pulsarHost ?? "nats://localhost:4222",
|
||||||
|
metricsPort,
|
||||||
|
manageProcessSignals: options.manageProcessSignals ?? true,
|
||||||
|
};
|
||||||
|
});
|
||||||
10
ts/packages/base/src/runtime/index.ts
Normal file
10
ts/packages/base/src/runtime/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export {
|
||||||
|
defaultMessagingRuntimeConfig,
|
||||||
|
loadMessagingRuntimeConfig,
|
||||||
|
type MessagingRuntimeConfig,
|
||||||
|
} from "./messaging-config.js";
|
||||||
|
export {
|
||||||
|
loadProcessorRuntimeConfig,
|
||||||
|
optionalStringConfig,
|
||||||
|
type ProcessorRuntimeConfigOptions,
|
||||||
|
} from "./config.js";
|
||||||
41
ts/packages/base/src/runtime/messaging-config.ts
Normal file
41
ts/packages/base/src/runtime/messaging-config.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* Effect Config contracts for messaging runtime behavior.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Config, Effect } from "effect";
|
||||||
|
|
||||||
|
export interface MessagingRuntimeConfig {
|
||||||
|
readonly consumerReceiveTimeoutMs: number;
|
||||||
|
readonly consumerErrorBackoffMs: number;
|
||||||
|
readonly rateLimitRetryMs: number;
|
||||||
|
readonly requestTimeoutMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultMessagingRuntimeConfig: MessagingRuntimeConfig = {
|
||||||
|
consumerReceiveTimeoutMs: 2_000,
|
||||||
|
consumerErrorBackoffMs: 1_000,
|
||||||
|
rateLimitRetryMs: 10_000,
|
||||||
|
requestTimeoutMs: 300_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadMessagingRuntimeConfig = Effect.fn("loadMessagingRuntimeConfig")(function* () {
|
||||||
|
const consumerReceiveTimeoutMs = yield* Config.number("TG_CONSUMER_RECEIVE_TIMEOUT_MS").pipe(
|
||||||
|
Config.withDefault(defaultMessagingRuntimeConfig.consumerReceiveTimeoutMs),
|
||||||
|
);
|
||||||
|
const consumerErrorBackoffMs = yield* Config.number("TG_CONSUMER_ERROR_BACKOFF_MS").pipe(
|
||||||
|
Config.withDefault(defaultMessagingRuntimeConfig.consumerErrorBackoffMs),
|
||||||
|
);
|
||||||
|
const rateLimitRetryMs = yield* Config.number("TG_RATE_LIMIT_RETRY_MS").pipe(
|
||||||
|
Config.withDefault(defaultMessagingRuntimeConfig.rateLimitRetryMs),
|
||||||
|
);
|
||||||
|
const requestTimeoutMs = yield* Config.number("TG_REQUEST_TIMEOUT_MS").pipe(
|
||||||
|
Config.withDefault(defaultMessagingRuntimeConfig.requestTimeoutMs),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
consumerReceiveTimeoutMs,
|
||||||
|
consumerErrorBackoffMs,
|
||||||
|
rateLimitRetryMs,
|
||||||
|
requestTimeoutMs,
|
||||||
|
} satisfies MessagingRuntimeConfig;
|
||||||
|
});
|
||||||
|
|
@ -1,344 +1,455 @@
|
||||||
/**
|
/**
|
||||||
* Message types for service communication.
|
* Schema-backed message types for service communication.
|
||||||
*
|
*
|
||||||
* Python reference: trustgraph-base/trustgraph/schema/services/
|
* Python reference: trustgraph-base/trustgraph/schema/services/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TgError, Triple, Term, RowSchema } from "./primitives.js";
|
import * as S from "effect/Schema";
|
||||||
|
import { Term, TgError, Triple } from "./primitives.js";
|
||||||
|
|
||||||
|
const UnknownRecord = S.Record(S.String, S.Unknown);
|
||||||
|
const MutableArray = <A extends S.Top>(schema: A) => schema.pipe(S.Array, S.mutable);
|
||||||
|
const OptionalMutableArray = <A extends S.Top>(schema: A) => schema.pipe(S.Array, S.mutable, S.optionalKey);
|
||||||
|
const StringArray = MutableArray(S.String);
|
||||||
|
const NumberArray = MutableArray(S.Number);
|
||||||
|
const NumberArrays = MutableArray(NumberArray);
|
||||||
|
|
||||||
// Text completion
|
// Text completion
|
||||||
export interface TextCompletionRequest {
|
export const TextCompletionRequest = S.Struct({
|
||||||
system: string;
|
system: S.String,
|
||||||
prompt: string;
|
prompt: S.String,
|
||||||
model?: string;
|
model: S.optionalKey(S.String),
|
||||||
temperature?: number;
|
temperature: S.optionalKey(S.Number),
|
||||||
streaming?: boolean;
|
streaming: S.optionalKey(S.Boolean),
|
||||||
}
|
});
|
||||||
|
export type TextCompletionRequest = typeof TextCompletionRequest.Type;
|
||||||
|
|
||||||
export interface TextCompletionResponse {
|
export const TextCompletionResponse = S.Struct({
|
||||||
response: string;
|
response: S.String,
|
||||||
model?: string;
|
model: S.optionalKey(S.String),
|
||||||
inToken?: number;
|
inToken: S.optionalKey(S.Number),
|
||||||
outToken?: number;
|
outToken: S.optionalKey(S.Number),
|
||||||
error?: TgError;
|
error: S.optionalKey(TgError),
|
||||||
endOfStream?: boolean;
|
endOfStream: S.optionalKey(S.Boolean),
|
||||||
}
|
});
|
||||||
|
export type TextCompletionResponse = typeof TextCompletionResponse.Type;
|
||||||
|
|
||||||
// Embeddings
|
// Embeddings
|
||||||
export interface EmbeddingsRequest {
|
export const EmbeddingsRequest = S.Struct({
|
||||||
text: string[];
|
text: StringArray,
|
||||||
model?: string;
|
model: S.optionalKey(S.String),
|
||||||
}
|
});
|
||||||
|
export type EmbeddingsRequest = typeof EmbeddingsRequest.Type;
|
||||||
|
|
||||||
export interface EmbeddingsResponse {
|
export const EmbeddingsResponse = S.Struct({
|
||||||
vectors: number[][];
|
vectors: NumberArrays,
|
||||||
error?: TgError;
|
error: S.optionalKey(TgError),
|
||||||
}
|
});
|
||||||
|
export type EmbeddingsResponse = typeof EmbeddingsResponse.Type;
|
||||||
|
|
||||||
// Graph RAG
|
// Graph RAG
|
||||||
export interface GraphRagRequest {
|
export const GraphRagRequest = S.Struct({
|
||||||
query: string;
|
query: S.String,
|
||||||
collection?: string;
|
collection: S.optionalKey(S.String),
|
||||||
entityLimit?: number;
|
entityLimit: S.optionalKey(S.Number),
|
||||||
tripleLimit?: number;
|
tripleLimit: S.optionalKey(S.Number),
|
||||||
maxSubgraphSize?: number;
|
maxSubgraphSize: S.optionalKey(S.Number),
|
||||||
maxPathLength?: number;
|
maxPathLength: S.optionalKey(S.Number),
|
||||||
streaming?: boolean;
|
streaming: S.optionalKey(S.Boolean),
|
||||||
}
|
});
|
||||||
|
export type GraphRagRequest = typeof GraphRagRequest.Type;
|
||||||
|
|
||||||
export interface GraphRagResponse {
|
export const GraphRagResponse = S.StructWithRest(
|
||||||
response: string;
|
S.Struct({
|
||||||
error?: TgError;
|
response: S.String,
|
||||||
endOfStream?: boolean;
|
error: S.optionalKey(TgError),
|
||||||
// Explainability: include retrieved subgraph triples
|
endOfStream: S.optionalKey(S.Boolean),
|
||||||
message_type?: "chunk" | "explain";
|
message_type: S.optionalKey(S.Union([S.Literal("chunk"), S.Literal("explain")])),
|
||||||
explain_id?: string;
|
explain_id: S.optionalKey(S.String),
|
||||||
explain_triples?: Triple[];
|
explain_triples: OptionalMutableArray(Triple),
|
||||||
[key: string]: unknown;
|
}),
|
||||||
}
|
[UnknownRecord],
|
||||||
|
);
|
||||||
|
export type GraphRagResponse = typeof GraphRagResponse.Type;
|
||||||
|
|
||||||
// Document RAG
|
// Document RAG
|
||||||
export interface DocumentRagRequest {
|
export const DocumentRagRequest = S.Struct({
|
||||||
query: string;
|
query: S.String,
|
||||||
collection?: string;
|
collection: S.optionalKey(S.String),
|
||||||
streaming?: boolean;
|
streaming: S.optionalKey(S.Boolean),
|
||||||
}
|
});
|
||||||
|
export type DocumentRagRequest = typeof DocumentRagRequest.Type;
|
||||||
|
|
||||||
export interface DocumentRagResponse {
|
export const DocumentRagResponse = S.Struct({
|
||||||
response: string;
|
response: S.String,
|
||||||
error?: TgError;
|
error: S.optionalKey(TgError),
|
||||||
endOfStream?: boolean;
|
endOfStream: S.optionalKey(S.Boolean),
|
||||||
}
|
});
|
||||||
|
export type DocumentRagResponse = typeof DocumentRagResponse.Type;
|
||||||
|
|
||||||
// Agent
|
// Agent
|
||||||
export interface AgentRequest {
|
export const AgentRequest = S.Struct({
|
||||||
question: string;
|
question: S.String,
|
||||||
collection?: string;
|
collection: S.optionalKey(S.String),
|
||||||
streaming?: boolean;
|
streaming: S.optionalKey(S.Boolean),
|
||||||
group?: string[];
|
group: S.optionalKey(StringArray),
|
||||||
state?: string;
|
state: S.optionalKey(S.String),
|
||||||
}
|
});
|
||||||
|
export type AgentRequest = typeof AgentRequest.Type;
|
||||||
|
|
||||||
export interface AgentResponse {
|
export const AgentResponse = S.Struct({
|
||||||
/** Streaming chunk type */
|
chunk_type: S.optionalKey(S.Union([
|
||||||
chunk_type?: "thought" | "observation" | "answer" | "error" | "explain";
|
S.Literal("thought"),
|
||||||
content?: string;
|
S.Literal("observation"),
|
||||||
end_of_message?: boolean;
|
S.Literal("answer"),
|
||||||
end_of_dialog?: boolean;
|
S.Literal("error"),
|
||||||
/** Legacy non-streaming fields */
|
S.Literal("explain"),
|
||||||
answer?: string;
|
])),
|
||||||
error?: TgError;
|
content: S.optionalKey(S.String),
|
||||||
endOfStream?: boolean;
|
end_of_message: S.optionalKey(S.Boolean),
|
||||||
endOfSession?: boolean;
|
end_of_dialog: S.optionalKey(S.Boolean),
|
||||||
/** Explainability fields */
|
answer: S.optionalKey(S.String),
|
||||||
explain_id?: string;
|
error: S.optionalKey(TgError),
|
||||||
explain_graph?: string;
|
endOfStream: S.optionalKey(S.Boolean),
|
||||||
explain_triples?: unknown[];
|
endOfSession: S.optionalKey(S.Boolean),
|
||||||
message_type?: string;
|
explain_id: S.optionalKey(S.String),
|
||||||
}
|
explain_graph: S.optionalKey(S.String),
|
||||||
|
explain_triples: OptionalMutableArray(S.Unknown),
|
||||||
|
message_type: S.optionalKey(S.String),
|
||||||
|
});
|
||||||
|
export type AgentResponse = typeof AgentResponse.Type;
|
||||||
|
|
||||||
// Triples query
|
// Triples query
|
||||||
export interface TriplesQueryRequest {
|
export const TriplesQueryRequest = S.Struct({
|
||||||
s?: Term;
|
s: S.optionalKey(Term),
|
||||||
p?: Term;
|
p: S.optionalKey(Term),
|
||||||
o?: Term;
|
o: S.optionalKey(Term),
|
||||||
collection?: string;
|
collection: S.optionalKey(S.String),
|
||||||
limit?: number;
|
limit: S.optionalKey(S.Number),
|
||||||
}
|
});
|
||||||
|
export type TriplesQueryRequest = typeof TriplesQueryRequest.Type;
|
||||||
|
|
||||||
export interface TriplesQueryResponse {
|
export const TriplesQueryResponse = S.Struct({
|
||||||
triples: Triple[];
|
triples: MutableArray(Triple),
|
||||||
error?: TgError;
|
error: S.optionalKey(TgError),
|
||||||
}
|
});
|
||||||
|
export type TriplesQueryResponse = typeof TriplesQueryResponse.Type;
|
||||||
|
|
||||||
// Graph embeddings query
|
// Graph embeddings query
|
||||||
export interface GraphEmbeddingsRequest {
|
export const GraphEmbeddingsRequest = S.Struct({
|
||||||
vectors: number[][];
|
vectors: NumberArrays,
|
||||||
user?: string;
|
user: S.optionalKey(S.String),
|
||||||
limit?: number;
|
limit: S.optionalKey(S.Number),
|
||||||
collection?: string;
|
collection: S.optionalKey(S.String),
|
||||||
}
|
});
|
||||||
|
export type GraphEmbeddingsRequest = typeof GraphEmbeddingsRequest.Type;
|
||||||
|
|
||||||
export interface GraphEmbeddingsResponse {
|
export const GraphEmbeddingsResponse = S.Struct({
|
||||||
entities: Term[];
|
entities: MutableArray(Term),
|
||||||
error?: TgError;
|
error: S.optionalKey(TgError),
|
||||||
}
|
});
|
||||||
|
export type GraphEmbeddingsResponse = typeof GraphEmbeddingsResponse.Type;
|
||||||
|
|
||||||
// Document embeddings query
|
// Document embeddings query
|
||||||
export interface DocumentEmbeddingsRequest {
|
export const DocumentEmbeddingsRequest = S.Struct({
|
||||||
vectors: number[][];
|
vectors: NumberArrays,
|
||||||
limit?: number;
|
limit: S.optionalKey(S.Number),
|
||||||
user?: string;
|
user: S.optionalKey(S.String),
|
||||||
collection?: string;
|
collection: S.optionalKey(S.String),
|
||||||
}
|
});
|
||||||
|
export type DocumentEmbeddingsRequest = typeof DocumentEmbeddingsRequest.Type;
|
||||||
|
|
||||||
export interface DocumentEmbeddingsResponse {
|
const DocumentEmbeddingChunk = S.Struct({
|
||||||
chunks: Array<{ chunkId: string; score: number; content?: string }>;
|
chunkId: S.String,
|
||||||
error?: TgError;
|
score: S.Number,
|
||||||
}
|
content: S.optionalKey(S.String),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DocumentEmbeddingsResponse = S.Struct({
|
||||||
|
chunks: MutableArray(DocumentEmbeddingChunk),
|
||||||
|
error: S.optionalKey(TgError),
|
||||||
|
});
|
||||||
|
export type DocumentEmbeddingsResponse = typeof DocumentEmbeddingsResponse.Type;
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
export type ConfigOperation = "get" | "list" | "delete" | "put" | "config" | "getvalues";
|
export const ConfigOperation = S.Union([
|
||||||
|
S.Literal("get"),
|
||||||
|
S.Literal("list"),
|
||||||
|
S.Literal("delete"),
|
||||||
|
S.Literal("put"),
|
||||||
|
S.Literal("config"),
|
||||||
|
S.Literal("getvalues"),
|
||||||
|
]);
|
||||||
|
export type ConfigOperation = typeof ConfigOperation.Type;
|
||||||
|
|
||||||
export interface ConfigRequest {
|
export const ConfigRequest = S.Struct({
|
||||||
operation: ConfigOperation;
|
operation: ConfigOperation,
|
||||||
keys?: string[];
|
keys: S.optionalKey(StringArray),
|
||||||
values?: Record<string, unknown>;
|
values: S.optionalKey(UnknownRecord),
|
||||||
type?: string;
|
type: S.optionalKey(S.String),
|
||||||
}
|
});
|
||||||
|
export type ConfigRequest = typeof ConfigRequest.Type;
|
||||||
|
|
||||||
export interface ConfigResponse {
|
export const ConfigResponse = S.Struct({
|
||||||
version?: number;
|
version: S.optionalKey(S.Number),
|
||||||
values?: Record<string, unknown>;
|
values: S.optionalKey(S.Unknown),
|
||||||
directory?: string[];
|
directory: S.optionalKey(StringArray),
|
||||||
config?: Record<string, unknown>;
|
config: S.optionalKey(UnknownRecord),
|
||||||
error?: TgError;
|
error: S.optionalKey(TgError),
|
||||||
}
|
});
|
||||||
|
export type ConfigResponse = typeof ConfigResponse.Type;
|
||||||
|
|
||||||
// Prompt
|
// Prompt
|
||||||
export interface PromptRequest {
|
export const PromptRequest = S.Struct({
|
||||||
name: string;
|
name: S.String,
|
||||||
variables?: Record<string, string>;
|
variables: S.optionalKey(S.Record(S.String, S.String)),
|
||||||
}
|
});
|
||||||
|
export type PromptRequest = typeof PromptRequest.Type;
|
||||||
|
|
||||||
export interface PromptResponse {
|
export const PromptResponse = S.Struct({
|
||||||
system: string;
|
system: S.String,
|
||||||
prompt: string;
|
prompt: S.String,
|
||||||
error?: TgError;
|
error: S.optionalKey(TgError),
|
||||||
}
|
});
|
||||||
|
export type PromptResponse = typeof PromptResponse.Type;
|
||||||
|
|
||||||
// ---------- Pipeline types ----------
|
// Pipeline types
|
||||||
|
export const PipelineMetadata = S.Struct({
|
||||||
|
id: S.String,
|
||||||
|
root: S.String,
|
||||||
|
user: S.String,
|
||||||
|
collection: S.String,
|
||||||
|
});
|
||||||
|
export type PipelineMetadata = typeof PipelineMetadata.Type;
|
||||||
|
|
||||||
export interface PipelineMetadata {
|
export const Document = S.Struct({
|
||||||
id: string;
|
metadata: PipelineMetadata,
|
||||||
root: string;
|
documentId: S.String,
|
||||||
user: string;
|
});
|
||||||
collection: string;
|
export type Document = typeof Document.Type;
|
||||||
}
|
|
||||||
|
|
||||||
/** Document message — triggers the decode pipeline for a librarian document. */
|
export const TextDocument = S.Struct({
|
||||||
export interface Document {
|
metadata: PipelineMetadata,
|
||||||
metadata: PipelineMetadata;
|
text: S.String,
|
||||||
documentId: string;
|
documentId: S.String,
|
||||||
}
|
});
|
||||||
|
export type TextDocument = typeof TextDocument.Type;
|
||||||
|
|
||||||
export interface TextDocument {
|
export const Chunk = S.Struct({
|
||||||
metadata: PipelineMetadata;
|
metadata: PipelineMetadata,
|
||||||
text: string;
|
chunk: S.String,
|
||||||
documentId: string;
|
documentId: S.String,
|
||||||
}
|
});
|
||||||
|
export type Chunk = typeof Chunk.Type;
|
||||||
|
|
||||||
export interface Chunk {
|
export const EntityContext = S.Struct({
|
||||||
metadata: PipelineMetadata;
|
entity: Term,
|
||||||
chunk: string;
|
context: S.String,
|
||||||
documentId: string;
|
chunkId: S.String,
|
||||||
}
|
});
|
||||||
|
export type EntityContext = typeof EntityContext.Type;
|
||||||
|
|
||||||
export interface EntityContext {
|
export const EntityContexts = S.Struct({
|
||||||
entity: Term;
|
metadata: PipelineMetadata,
|
||||||
context: string;
|
entities: MutableArray(EntityContext),
|
||||||
chunkId: string;
|
});
|
||||||
}
|
export type EntityContexts = typeof EntityContexts.Type;
|
||||||
|
|
||||||
export interface EntityContexts {
|
export const Triples = S.Struct({
|
||||||
metadata: PipelineMetadata;
|
metadata: PipelineMetadata,
|
||||||
entities: EntityContext[];
|
triples: MutableArray(Triple),
|
||||||
}
|
});
|
||||||
|
export type Triples = typeof Triples.Type;
|
||||||
|
|
||||||
export interface Triples {
|
// Document metadata
|
||||||
metadata: PipelineMetadata;
|
export const DocumentMetadata = S.Struct({
|
||||||
triples: Triple[];
|
id: S.String,
|
||||||
}
|
time: S.Number,
|
||||||
|
kind: S.String,
|
||||||
|
title: S.String,
|
||||||
|
comments: S.String,
|
||||||
|
user: S.String,
|
||||||
|
tags: StringArray,
|
||||||
|
parentId: S.optionalKey(S.String),
|
||||||
|
documentType: S.String,
|
||||||
|
metadata: OptionalMutableArray(Triple),
|
||||||
|
});
|
||||||
|
export type DocumentMetadata = typeof DocumentMetadata.Type;
|
||||||
|
|
||||||
// ---------- Document metadata ----------
|
export const ProcessingMetadata = S.Struct({
|
||||||
|
id: S.String,
|
||||||
|
documentId: S.String,
|
||||||
|
time: S.Number,
|
||||||
|
flow: S.String,
|
||||||
|
user: S.String,
|
||||||
|
collection: S.String,
|
||||||
|
tags: StringArray,
|
||||||
|
});
|
||||||
|
export type ProcessingMetadata = typeof ProcessingMetadata.Type;
|
||||||
|
|
||||||
export interface DocumentMetadata {
|
// Librarian
|
||||||
id: string;
|
export const LibrarianOperation = S.Literals([
|
||||||
time: number;
|
"add-document",
|
||||||
kind: string;
|
"remove-document",
|
||||||
title: string;
|
"list-documents",
|
||||||
comments: string;
|
"get-document-metadata",
|
||||||
user: string;
|
"get-document-content",
|
||||||
tags: string[];
|
"add-child-document",
|
||||||
parentId?: string;
|
"list-children",
|
||||||
documentType: string; // "source" | "page" | "chunk" | "extracted"
|
"add-processing",
|
||||||
metadata?: Triple[];
|
"remove-processing",
|
||||||
}
|
"list-processing",
|
||||||
|
]);
|
||||||
|
export type LibrarianOperation = typeof LibrarianOperation.Type;
|
||||||
|
|
||||||
export interface ProcessingMetadata {
|
export const LibrarianRequest = S.Struct({
|
||||||
id: string;
|
operation: LibrarianOperation,
|
||||||
documentId: string;
|
documentId: S.optionalKey(S.String),
|
||||||
time: number;
|
processingId: S.optionalKey(S.String),
|
||||||
flow: string;
|
documentMetadata: S.optionalKey(DocumentMetadata),
|
||||||
user: string;
|
processingMetadata: S.optionalKey(ProcessingMetadata),
|
||||||
collection: string;
|
content: S.optionalKey(S.String),
|
||||||
tags: string[];
|
user: S.optionalKey(S.String),
|
||||||
}
|
collection: S.optionalKey(S.String),
|
||||||
|
});
|
||||||
|
export type LibrarianRequest = typeof LibrarianRequest.Type;
|
||||||
|
|
||||||
// ---------- Librarian ----------
|
export const LibrarianResponse = S.Struct({
|
||||||
|
error: S.optionalKey(TgError),
|
||||||
|
documentMetadata: S.optionalKey(DocumentMetadata),
|
||||||
|
content: S.optionalKey(S.String),
|
||||||
|
documents: OptionalMutableArray(DocumentMetadata),
|
||||||
|
processing: OptionalMutableArray(ProcessingMetadata),
|
||||||
|
});
|
||||||
|
export type LibrarianResponse = typeof LibrarianResponse.Type;
|
||||||
|
|
||||||
export type LibrarianOperation =
|
// Knowledge core
|
||||||
| "add-document"
|
export const KnowledgeOperation = S.Literals([
|
||||||
| "remove-document"
|
"list-kg-cores",
|
||||||
| "list-documents"
|
"get-kg-core",
|
||||||
| "get-document-metadata"
|
"delete-kg-core",
|
||||||
| "get-document-content"
|
"put-kg-core",
|
||||||
| "add-child-document"
|
"load-kg-core",
|
||||||
| "list-children"
|
]);
|
||||||
| "add-processing"
|
export type KnowledgeOperation = typeof KnowledgeOperation.Type;
|
||||||
| "remove-processing"
|
|
||||||
| "list-processing";
|
|
||||||
|
|
||||||
export interface LibrarianRequest {
|
const GraphEmbedding = S.Struct({
|
||||||
operation: LibrarianOperation;
|
entity: Term,
|
||||||
documentId?: string;
|
vectors: NumberArrays,
|
||||||
processingId?: string;
|
});
|
||||||
documentMetadata?: DocumentMetadata;
|
|
||||||
processingMetadata?: ProcessingMetadata;
|
|
||||||
content?: string; // base64
|
|
||||||
user?: string;
|
|
||||||
collection?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LibrarianResponse {
|
export const KnowledgeRequest = S.Struct({
|
||||||
error?: TgError;
|
operation: KnowledgeOperation,
|
||||||
documentMetadata?: DocumentMetadata;
|
user: S.optionalKey(S.String),
|
||||||
content?: string; // base64
|
id: S.optionalKey(S.String),
|
||||||
documents?: DocumentMetadata[];
|
flow: S.optionalKey(S.String),
|
||||||
processing?: ProcessingMetadata[];
|
collection: S.optionalKey(S.String),
|
||||||
}
|
triples: OptionalMutableArray(Triple),
|
||||||
|
graphEmbeddings: OptionalMutableArray(GraphEmbedding),
|
||||||
|
});
|
||||||
|
export type KnowledgeRequest = typeof KnowledgeRequest.Type;
|
||||||
|
|
||||||
// ---------- Knowledge core ----------
|
export const KnowledgeResponse = S.Struct({
|
||||||
|
error: S.optionalKey(TgError),
|
||||||
|
ids: S.optionalKey(StringArray),
|
||||||
|
eos: S.optionalKey(S.Boolean),
|
||||||
|
triples: OptionalMutableArray(Triple),
|
||||||
|
graphEmbeddings: OptionalMutableArray(GraphEmbedding),
|
||||||
|
});
|
||||||
|
export type KnowledgeResponse = typeof KnowledgeResponse.Type;
|
||||||
|
|
||||||
export type KnowledgeOperation =
|
// Collection management
|
||||||
| "list-kg-cores"
|
export const CollectionOperation = S.Literals([
|
||||||
| "get-kg-core"
|
"list-collections",
|
||||||
| "delete-kg-core"
|
"update-collection",
|
||||||
| "put-kg-core"
|
"delete-collection",
|
||||||
| "load-kg-core";
|
]);
|
||||||
|
export type CollectionOperation = typeof CollectionOperation.Type;
|
||||||
|
|
||||||
export interface KnowledgeRequest {
|
const CollectionEntry = S.Struct({
|
||||||
operation: KnowledgeOperation;
|
user: S.String,
|
||||||
user?: string;
|
collection: S.String,
|
||||||
id?: string;
|
name: S.String,
|
||||||
flow?: string;
|
description: S.String,
|
||||||
collection?: string;
|
tags: StringArray,
|
||||||
triples?: Triple[];
|
});
|
||||||
graphEmbeddings?: { entity: Term; vectors: number[][] }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KnowledgeResponse {
|
export const CollectionManagementRequest = S.Struct({
|
||||||
error?: TgError;
|
operation: CollectionOperation,
|
||||||
ids?: string[];
|
user: S.optionalKey(S.String),
|
||||||
eos?: boolean;
|
collection: S.optionalKey(S.String),
|
||||||
triples?: Triple[];
|
name: S.optionalKey(S.String),
|
||||||
graphEmbeddings?: { entity: Term; vectors: number[][] }[];
|
description: S.optionalKey(S.String),
|
||||||
}
|
tags: S.optionalKey(StringArray),
|
||||||
|
});
|
||||||
|
export type CollectionManagementRequest = typeof CollectionManagementRequest.Type;
|
||||||
|
|
||||||
// ---------- Collection management ----------
|
export const CollectionManagementResponse = S.Struct({
|
||||||
|
error: S.optionalKey(TgError),
|
||||||
|
collections: OptionalMutableArray(CollectionEntry),
|
||||||
|
});
|
||||||
|
export type CollectionManagementResponse = typeof CollectionManagementResponse.Type;
|
||||||
|
|
||||||
export type CollectionOperation =
|
// Tool invocation (MCP tools)
|
||||||
| "list-collections"
|
export const ToolRequest = S.Struct({
|
||||||
| "update-collection"
|
name: S.String,
|
||||||
| "delete-collection";
|
parameters: S.String,
|
||||||
|
});
|
||||||
|
export type ToolRequest = typeof ToolRequest.Type;
|
||||||
|
|
||||||
export interface CollectionManagementRequest {
|
export const ToolResponse = S.Struct({
|
||||||
operation: CollectionOperation;
|
error: S.optionalKey(TgError),
|
||||||
user?: string;
|
text: S.optionalKey(S.String),
|
||||||
collection?: string;
|
object: S.optionalKey(S.String),
|
||||||
name?: string;
|
});
|
||||||
description?: string;
|
export type ToolResponse = typeof ToolResponse.Type;
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CollectionManagementResponse {
|
// Flow management
|
||||||
error?: TgError;
|
export const FlowRequest = S.StructWithRest(
|
||||||
collections?: { user: string; collection: string; name: string; description: string; tags: string[] }[];
|
S.Struct({
|
||||||
}
|
operation: S.String,
|
||||||
|
}),
|
||||||
|
[UnknownRecord],
|
||||||
|
);
|
||||||
|
export type FlowRequest = typeof FlowRequest.Type;
|
||||||
|
|
||||||
// ---------- Tool invocation (MCP tools) ----------
|
export const FlowResponse = S.StructWithRest(
|
||||||
|
S.Struct({
|
||||||
|
error: S.optionalKey(TgError),
|
||||||
|
}),
|
||||||
|
[UnknownRecord],
|
||||||
|
);
|
||||||
|
export type FlowResponse = typeof FlowResponse.Type;
|
||||||
|
|
||||||
export interface ToolRequest {
|
export const ServiceMessageSchemas = {
|
||||||
name: string;
|
TextCompletionRequest,
|
||||||
parameters: string; // JSON-encoded
|
TextCompletionResponse,
|
||||||
}
|
EmbeddingsRequest,
|
||||||
|
EmbeddingsResponse,
|
||||||
export interface ToolResponse {
|
GraphRagRequest,
|
||||||
error?: TgError;
|
GraphRagResponse,
|
||||||
text?: string; // Plain text response
|
DocumentRagRequest,
|
||||||
object?: string; // JSON-encoded structured response
|
DocumentRagResponse,
|
||||||
}
|
AgentRequest,
|
||||||
|
AgentResponse,
|
||||||
// ---------- Flow management ----------
|
TriplesQueryRequest,
|
||||||
|
TriplesQueryResponse,
|
||||||
// Flow request/response use kebab-case wire format to match the client.
|
GraphEmbeddingsRequest,
|
||||||
// Access fields via bracket notation: request["flow-id"]
|
GraphEmbeddingsResponse,
|
||||||
export interface FlowRequest {
|
DocumentEmbeddingsRequest,
|
||||||
operation: string;
|
DocumentEmbeddingsResponse,
|
||||||
[key: string]: unknown;
|
ConfigRequest,
|
||||||
}
|
ConfigResponse,
|
||||||
|
PromptRequest,
|
||||||
export interface FlowResponse {
|
PromptResponse,
|
||||||
error?: TgError;
|
LibrarianRequest,
|
||||||
[key: string]: unknown;
|
LibrarianResponse,
|
||||||
}
|
KnowledgeRequest,
|
||||||
|
KnowledgeResponse,
|
||||||
|
CollectionManagementRequest,
|
||||||
|
CollectionManagementResponse,
|
||||||
|
ToolRequest,
|
||||||
|
ToolResponse,
|
||||||
|
FlowRequest,
|
||||||
|
FlowResponse,
|
||||||
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,102 @@
|
||||||
/**
|
/**
|
||||||
* Core data types mirroring the Python schema primitives.
|
* Schema-backed core data types mirroring the Python schema primitives.
|
||||||
*
|
*
|
||||||
* Python reference: trustgraph-base/trustgraph/schema/core/primitives.py
|
* Python reference: trustgraph-base/trustgraph/schema/core/primitives.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface TgError {
|
import * as S from "effect/Schema";
|
||||||
type: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RDF Term types — discriminated union
|
export const TgError = S.Struct({
|
||||||
export type TermType = "IRI" | "BLANK" | "LITERAL" | "TRIPLE";
|
type: S.String,
|
||||||
|
message: S.String,
|
||||||
|
});
|
||||||
|
export type TgError = typeof TgError.Type;
|
||||||
|
|
||||||
export interface IriTerm {
|
export const TermType = S.Literals([
|
||||||
type: "IRI";
|
"IRI",
|
||||||
iri: string;
|
"BLANK",
|
||||||
}
|
"LITERAL",
|
||||||
|
"TRIPLE",
|
||||||
|
]);
|
||||||
|
export type TermType = typeof TermType.Type;
|
||||||
|
|
||||||
export interface BlankTerm {
|
export const IriTerm = S.Struct({
|
||||||
type: "BLANK";
|
type: S.tag("IRI"),
|
||||||
id: string;
|
iri: S.String,
|
||||||
}
|
});
|
||||||
|
export type IriTerm = typeof IriTerm.Type;
|
||||||
|
|
||||||
export interface LiteralTerm {
|
export const BlankTerm = S.Struct({
|
||||||
type: "LITERAL";
|
type: S.tag("BLANK"),
|
||||||
value: string;
|
id: S.String,
|
||||||
datatype?: string;
|
});
|
||||||
language?: string;
|
export type BlankTerm = typeof BlankTerm.Type;
|
||||||
}
|
|
||||||
|
|
||||||
export interface TripleTerm {
|
export const LiteralTerm = S.Struct({
|
||||||
type: "TRIPLE";
|
type: S.tag("LITERAL"),
|
||||||
triple: Triple;
|
value: S.String,
|
||||||
}
|
datatype: S.optionalKey(S.String),
|
||||||
|
language: S.optionalKey(S.String),
|
||||||
|
});
|
||||||
|
export type LiteralTerm = typeof LiteralTerm.Type;
|
||||||
|
|
||||||
export type Term = IriTerm | BlankTerm | LiteralTerm | TripleTerm;
|
export type Term = IriTerm | BlankTerm | LiteralTerm | TripleTerm;
|
||||||
|
export type Triple = {
|
||||||
|
readonly s: Term;
|
||||||
|
readonly p: Term;
|
||||||
|
readonly o: Term;
|
||||||
|
readonly g?: Term;
|
||||||
|
};
|
||||||
|
|
||||||
export interface Triple {
|
export const Triple: S.Codec<Triple, Triple> = S.suspend(() =>
|
||||||
s: Term;
|
S.Struct({
|
||||||
p: Term;
|
s: Term,
|
||||||
o: Term;
|
p: Term,
|
||||||
g?: Term; // Named graph (optional quad)
|
o: Term,
|
||||||
|
g: S.optionalKey(Term),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TripleTerm: S.Codec<TripleTerm, TripleTerm> = S.suspend(() =>
|
||||||
|
S.Struct({
|
||||||
|
type: S.tag("TRIPLE"),
|
||||||
|
triple: Triple,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
export interface TripleTerm {
|
||||||
|
readonly type: "TRIPLE";
|
||||||
|
readonly triple: Triple;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Field {
|
export const Term: S.Codec<Term, Term> = S.suspend(() => S.Union([IriTerm, BlankTerm, LiteralTerm, TripleTerm]));
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RowSchema {
|
export const Field = S.Struct({
|
||||||
name: string;
|
name: S.String,
|
||||||
description?: string;
|
type: S.String,
|
||||||
fields: Field[];
|
description: S.optionalKey(S.String),
|
||||||
}
|
});
|
||||||
|
export type Field = typeof Field.Type;
|
||||||
|
|
||||||
// LLM-related types
|
export const RowSchema = S.Struct({
|
||||||
export interface LlmResult {
|
name: S.String,
|
||||||
text: string;
|
description: S.optionalKey(S.String),
|
||||||
inToken: number;
|
fields: S.Array(Field).pipe(S.mutable),
|
||||||
outToken: number;
|
});
|
||||||
model: string;
|
export type RowSchema = typeof RowSchema.Type;
|
||||||
}
|
|
||||||
|
|
||||||
export interface LlmChunk {
|
export const LlmResult = S.Struct({
|
||||||
text: string;
|
text: S.String,
|
||||||
inToken: number | null;
|
inToken: S.Number,
|
||||||
outToken: number | null;
|
outToken: S.Number,
|
||||||
model: string;
|
model: S.String,
|
||||||
isFinal: boolean;
|
});
|
||||||
}
|
export type LlmResult = typeof LlmResult.Type;
|
||||||
|
|
||||||
|
export const LlmChunk = S.Struct({
|
||||||
|
text: S.String,
|
||||||
|
inToken: S.NullOr(S.Number),
|
||||||
|
outToken: S.NullOr(S.Number),
|
||||||
|
model: S.String,
|
||||||
|
isFinal: S.Boolean,
|
||||||
|
});
|
||||||
|
export type LlmChunk = typeof LlmChunk.Type;
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,82 @@
|
||||||
/**
|
/**
|
||||||
* Base embeddings service.
|
* Embeddings capability contract and message-bus adapter.
|
||||||
*
|
*
|
||||||
* Python reference: trustgraph-base/trustgraph/base/embeddings_service.py
|
* Python reference: trustgraph-base/trustgraph/base/embeddings_service.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FlowProcessor } from "../processor/flow-processor.js";
|
import { Context, Effect } from "effect";
|
||||||
import { ConsumerSpec } from "../spec/consumer-spec.js";
|
import {
|
||||||
import { ProducerSpec } from "../spec/producer-spec.js";
|
errorMessage,
|
||||||
import { ParameterSpec } from "../spec/parameter-spec.js";
|
type EmbeddingsError,
|
||||||
import type { ProcessorConfig } from "../processor/async-processor.js";
|
type FlowResourceNotFoundError,
|
||||||
|
type MessagingDeliveryError,
|
||||||
|
} from "../errors.js";
|
||||||
import type { FlowContext } from "../messaging/consumer.js";
|
import type { FlowContext } from "../messaging/consumer.js";
|
||||||
|
import { FlowProcessor } from "../processor/flow-processor.js";
|
||||||
|
import type { ProcessorConfig } from "../processor/async-processor.js";
|
||||||
import type { EmbeddingsRequest, EmbeddingsResponse } from "../schema/messages.js";
|
import type { EmbeddingsRequest, EmbeddingsResponse } from "../schema/messages.js";
|
||||||
|
import { ConsumerSpec } from "../spec/consumer-spec.js";
|
||||||
|
import { ParameterSpec } from "../spec/parameter-spec.js";
|
||||||
|
import { ProducerSpec } from "../spec/producer-spec.js";
|
||||||
|
|
||||||
export abstract class EmbeddingsService extends FlowProcessor {
|
export interface EmbeddingsServiceShape {
|
||||||
protected constructor(config: ProcessorConfig) {
|
readonly embed: (
|
||||||
|
texts: ReadonlyArray<string>,
|
||||||
|
model?: string,
|
||||||
|
) => Effect.Effect<number[][], EmbeddingsError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Embeddings extends Context.Service<Embeddings, EmbeddingsServiceShape>()(
|
||||||
|
"@trustgraph/base/services/embeddings-service/Embeddings",
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class EmbeddingsService extends FlowProcessor<Embeddings> {
|
||||||
|
constructor(config: ProcessorConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
this.registerSpecification(
|
this.registerSpecification(
|
||||||
new ConsumerSpec<EmbeddingsRequest>(
|
new ConsumerSpec<EmbeddingsRequest, FlowResourceNotFoundError | MessagingDeliveryError, Embeddings>(
|
||||||
"embeddings-request",
|
"embeddings-request",
|
||||||
this.onRequest.bind(this),
|
this.onRequestEffect.bind(this),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
this.registerSpecification(new ProducerSpec<EmbeddingsResponse>("embeddings-response"));
|
this.registerSpecification(new ProducerSpec<EmbeddingsResponse>("embeddings-response"));
|
||||||
this.registerSpecification(new ParameterSpec("model"));
|
this.registerSpecification(new ParameterSpec("model"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onRequest(
|
private onRequestEffect(
|
||||||
msg: EmbeddingsRequest,
|
msg: EmbeddingsRequest,
|
||||||
properties: Record<string, string>,
|
properties: Record<string, string>,
|
||||||
flowCtx: FlowContext,
|
flowCtx: FlowContext<Embeddings>,
|
||||||
): Promise<void> {
|
): Effect.Effect<void, FlowResourceNotFoundError | MessagingDeliveryError, Embeddings> {
|
||||||
const requestId = properties.id;
|
const requestId = properties.id;
|
||||||
if (!requestId) return;
|
if (requestId === undefined || requestId.length === 0) {
|
||||||
|
return Effect.void;
|
||||||
const responseProducer = flowCtx.flow.producer<EmbeddingsResponse>("embeddings-response");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const vectors = await this.onEmbeddings(msg.text, msg.model);
|
|
||||||
await responseProducer.send(requestId, { vectors });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[EmbeddingsService] Error processing request:`, err);
|
|
||||||
|
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
|
||||||
await responseProducer.send(requestId, {
|
|
||||||
vectors: [],
|
|
||||||
error: { type: "embeddings-error", message },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
abstract onEmbeddings(texts: string[], model?: string): Promise<number[][]>;
|
return Effect.gen(function* () {
|
||||||
|
const responseProducer = yield* flowCtx.flow.producerEffect<EmbeddingsResponse>("embeddings-response");
|
||||||
|
const embeddings = yield* Embeddings;
|
||||||
|
const response = yield* embeddings.embed(msg.text, msg.model).pipe(
|
||||||
|
Effect.map((vectors) => ({ vectors }) satisfies EmbeddingsResponse),
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.logError("[EmbeddingsService] Error processing request", {
|
||||||
|
error: errorMessage(error),
|
||||||
|
operation: error.operation,
|
||||||
|
provider: error.provider ?? "unknown",
|
||||||
|
}).pipe(
|
||||||
|
Effect.as({
|
||||||
|
vectors: [],
|
||||||
|
error: {
|
||||||
|
type: "embeddings-error",
|
||||||
|
message: errorMessage(error),
|
||||||
|
},
|
||||||
|
} satisfies EmbeddingsResponse),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
yield* responseProducer.send(requestId, response);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,6 @@
|
||||||
export { LlmService } from "./llm-service.js";
|
export { LlmService } from "./llm-service.js";
|
||||||
export { EmbeddingsService } from "./embeddings-service.js";
|
export {
|
||||||
|
Embeddings,
|
||||||
|
EmbeddingsService,
|
||||||
|
type EmbeddingsServiceShape,
|
||||||
|
} from "./embeddings-service.js";
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export abstract class LlmService extends FlowProcessor {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
this.registerSpecification(
|
this.registerSpecification(
|
||||||
new ConsumerSpec<TextCompletionRequest>(
|
ConsumerSpec.fromPromise<TextCompletionRequest>(
|
||||||
"text-completion-request",
|
"text-completion-request",
|
||||||
this.onRequest.bind(this),
|
this.onRequest.bind(this),
|
||||||
),
|
),
|
||||||
|
|
@ -36,50 +36,52 @@ export abstract class LlmService extends FlowProcessor {
|
||||||
msg: TextCompletionRequest,
|
msg: TextCompletionRequest,
|
||||||
properties: Record<string, string>,
|
properties: Record<string, string>,
|
||||||
flowCtx: FlowContext,
|
flowCtx: FlowContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const requestId = properties.id;
|
const requestId = properties.id;
|
||||||
if (!requestId) return;
|
if (requestId === undefined || requestId.length === 0) return;
|
||||||
|
|
||||||
const responseProducer = flowCtx.flow.producer<TextCompletionResponse>("text-completion-response");
|
const responseProducer = flowCtx.flow.producer<TextCompletionResponse>("text-completion-response");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (msg.streaming && this.supportsStreaming()) {
|
if (msg.streaming === true && this.supportsStreaming()) {
|
||||||
for await (const chunk of this.generateContentStream(
|
for await (const chunk of this.generateContentStream(
|
||||||
msg.system,
|
msg.system,
|
||||||
msg.prompt,
|
msg.prompt,
|
||||||
msg.model,
|
msg.model,
|
||||||
msg.temperature,
|
msg.temperature,
|
||||||
)) {
|
)) {
|
||||||
await responseProducer.send(
|
const response = {
|
||||||
requestId,
|
|
||||||
{
|
|
||||||
response: chunk.text,
|
response: chunk.text,
|
||||||
model: chunk.model,
|
...(chunk.model !== undefined ? { model: chunk.model } : {}),
|
||||||
inToken: chunk.inToken ?? undefined,
|
...(chunk.inToken !== null ? { inToken: chunk.inToken } : {}),
|
||||||
outToken: chunk.outToken ?? undefined,
|
...(chunk.outToken !== null ? { outToken: chunk.outToken } : {}),
|
||||||
endOfStream: chunk.isFinal,
|
endOfStream: chunk.isFinal,
|
||||||
}
|
};
|
||||||
);
|
await responseProducer.send(
|
||||||
}
|
requestId,
|
||||||
} else {
|
response
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const result = await this.generateContent(
|
const result = await this.generateContent(
|
||||||
msg.system,
|
msg.system,
|
||||||
msg.prompt,
|
msg.prompt,
|
||||||
msg.model,
|
msg.model,
|
||||||
msg.temperature,
|
msg.temperature,
|
||||||
);
|
);
|
||||||
|
const response = {
|
||||||
await responseProducer.send(
|
|
||||||
requestId,
|
|
||||||
{
|
|
||||||
response: result.text,
|
response: result.text,
|
||||||
model: result.model,
|
...(result.model !== undefined ? { model: result.model } : {}),
|
||||||
inToken: result.inToken,
|
...(result.inToken !== undefined ? { inToken: result.inToken } : {}),
|
||||||
outToken: result.outToken,
|
...(result.outToken !== undefined ? { outToken: result.outToken } : {}),
|
||||||
endOfStream: true,
|
endOfStream: true,
|
||||||
}
|
};
|
||||||
);
|
|
||||||
}
|
await responseProducer.send(
|
||||||
|
requestId,
|
||||||
|
response
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
`[LlmService] Error processing request:`,
|
`[LlmService] Error processing request:`,
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,84 @@
|
||||||
* Python reference: trustgraph-base/trustgraph/base/consumer_spec.py
|
* Python reference: trustgraph-base/trustgraph/base/consumer_spec.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Effect } from "effect";
|
||||||
|
import * as S from "effect/Schema";
|
||||||
import type { Spec } from "./types.js";
|
import type { Spec } from "./types.js";
|
||||||
|
import type { SpecRuntimeRequirements } from "./types.js";
|
||||||
import type { PubSubBackend } from "../backend/types.js";
|
import type { PubSubBackend } from "../backend/types.js";
|
||||||
import type { Flow, FlowDefinition } from "../processor/flow.js";
|
import type { Flow, FlowDefinition } from "../processor/flow.js";
|
||||||
import { Consumer, type MessageHandler } from "../messaging/consumer.js";
|
import { type MessageHandler } from "../messaging/consumer.js";
|
||||||
|
import {
|
||||||
|
ConsumerFactory,
|
||||||
|
type EffectMessageHandler,
|
||||||
|
} from "../messaging/runtime.js";
|
||||||
|
import {
|
||||||
|
messagingHandlerError,
|
||||||
|
TooManyRequestsError,
|
||||||
|
type MessagingHandlerError,
|
||||||
|
type PubSubError,
|
||||||
|
} from "../errors.js";
|
||||||
|
|
||||||
|
const isTooManyRequestsError = S.is(TooManyRequestsError);
|
||||||
|
|
||||||
|
export class ConsumerSpec<T, E = never, R = never> implements Spec<R> {
|
||||||
|
public readonly name: string;
|
||||||
|
private readonly handler: EffectMessageHandler<T, E, R>;
|
||||||
|
private readonly concurrency: number;
|
||||||
|
|
||||||
export class ConsumerSpec<T> implements Spec {
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
name: string,
|
||||||
private readonly handler: MessageHandler<T>,
|
handler: EffectMessageHandler<T, E, R>,
|
||||||
private readonly concurrency = 1,
|
concurrency = 1,
|
||||||
) {}
|
) {
|
||||||
|
this.name = name;
|
||||||
|
this.handler = handler;
|
||||||
|
this.concurrency = concurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromPromise<T>(
|
||||||
|
name: string,
|
||||||
|
handler: MessageHandler<T>,
|
||||||
|
concurrency = 1,
|
||||||
|
): ConsumerSpec<T, TooManyRequestsError | MessagingHandlerError> {
|
||||||
|
return new ConsumerSpec<T, TooManyRequestsError | MessagingHandlerError>(
|
||||||
|
name,
|
||||||
|
(message, properties, flow) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => handler(message, properties, flow),
|
||||||
|
catch: (error) =>
|
||||||
|
isTooManyRequestsError(error)
|
||||||
|
? error
|
||||||
|
: messagingHandlerError(name, `${flow.id}-${flow.name}-${name}`, error),
|
||||||
|
}),
|
||||||
|
concurrency,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEffect(flow: Flow<R>, definition: FlowDefinition) {
|
||||||
|
const spec = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const topic = definition.topics?.[spec.name] ?? spec.name;
|
||||||
|
const factory = yield* ConsumerFactory;
|
||||||
|
const consumer = yield* factory.run<T, E, R>(
|
||||||
|
{
|
||||||
|
topic,
|
||||||
|
subscription: `${flow.processorId}-${flow.name}-${spec.name}`,
|
||||||
|
handler: spec.handler,
|
||||||
|
concurrency: spec.concurrency,
|
||||||
|
},
|
||||||
|
{ id: flow.processorId, name: flow.name, flow },
|
||||||
|
);
|
||||||
|
flow.registerConsumer(spec.name, consumer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async add(flow: Flow, pubsub: PubSubBackend, definition: FlowDefinition): Promise<void> {
|
async add(flow: Flow, pubsub: PubSubBackend, definition: FlowDefinition): Promise<void> {
|
||||||
const topic = definition.topics?.[this.name] ?? this.name;
|
const effect = this.addEffect(flow, definition) as Effect.Effect<
|
||||||
|
void,
|
||||||
const consumer = new Consumer<T>({
|
PubSubError,
|
||||||
pubsub,
|
SpecRuntimeRequirements
|
||||||
topic,
|
>;
|
||||||
subscription: `${flow.processorId}-${flow.name}-${this.name}`,
|
await flow.runInCompatibilityScope(effect, pubsub);
|
||||||
handler: this.handler,
|
|
||||||
concurrency: this.concurrency,
|
|
||||||
});
|
|
||||||
|
|
||||||
flow.registerConsumer(this.name, consumer as Consumer<unknown>);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export type { Spec } from "./types.js";
|
export type { Spec, SpecRuntimeError, SpecRuntimeRequirements } from "./types.js";
|
||||||
export { ConsumerSpec } from "./consumer-spec.js";
|
export { ConsumerSpec } from "./consumer-spec.js";
|
||||||
export { ProducerSpec } from "./producer-spec.js";
|
export { ProducerSpec } from "./producer-spec.js";
|
||||||
export { ParameterSpec } from "./parameter-spec.js";
|
export { ParameterSpec } from "./parameter-spec.js";
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,27 @@
|
||||||
* Python reference: trustgraph-base/trustgraph/base/parameter_spec.py
|
* Python reference: trustgraph-base/trustgraph/base/parameter_spec.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Effect } from "effect";
|
||||||
import type { Spec } from "./types.js";
|
import type { Spec } from "./types.js";
|
||||||
import type { PubSubBackend } from "../backend/types.js";
|
import type { PubSubBackend } from "../backend/types.js";
|
||||||
import type { Flow, FlowDefinition } from "../processor/flow.js";
|
import type { Flow, FlowDefinition } from "../processor/flow.js";
|
||||||
|
|
||||||
export class ParameterSpec implements Spec {
|
export class ParameterSpec implements Spec {
|
||||||
constructor(public readonly name: string) {}
|
public readonly name: string;
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEffect(flow: Flow, definition: FlowDefinition) {
|
||||||
|
const spec = this;
|
||||||
|
return Effect.sync(() => {
|
||||||
|
const value = definition.parameters?.[spec.name];
|
||||||
|
flow.setParameter(spec.name, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async add(flow: Flow, _pubsub: PubSubBackend, definition: FlowDefinition): Promise<void> {
|
async add(flow: Flow, _pubsub: PubSubBackend, definition: FlowDefinition): Promise<void> {
|
||||||
const value = definition.parameters?.[this.name];
|
await Effect.runPromise(this.addEffect(flow, definition));
|
||||||
flow.setParameter(this.name, value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,33 @@
|
||||||
* Python reference: trustgraph-base/trustgraph/base/producer_spec.py
|
* Python reference: trustgraph-base/trustgraph/base/producer_spec.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Effect } from "effect";
|
||||||
import type { Spec } from "./types.js";
|
import type { Spec } from "./types.js";
|
||||||
import type { PubSubBackend } from "../backend/types.js";
|
import type { PubSubBackend } from "../backend/types.js";
|
||||||
import type { Flow, FlowDefinition } from "../processor/flow.js";
|
import type { Flow, FlowDefinition } from "../processor/flow.js";
|
||||||
import { Producer } from "../messaging/producer.js";
|
import {
|
||||||
|
ProducerFactory,
|
||||||
|
type EffectProducer,
|
||||||
|
} from "../messaging/runtime.js";
|
||||||
|
|
||||||
export class ProducerSpec<T> implements Spec {
|
export class ProducerSpec<T> implements Spec {
|
||||||
constructor(public readonly name: string) {}
|
public readonly name: string;
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEffect(flow: Flow, definition: FlowDefinition) {
|
||||||
|
const spec = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const topic = definition.topics?.[spec.name] ?? spec.name;
|
||||||
|
const factory = yield* ProducerFactory;
|
||||||
|
const producer = yield* factory.make<T>({ topic });
|
||||||
|
flow.registerProducer(spec.name, producer as EffectProducer<unknown>);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async add(flow: Flow, pubsub: PubSubBackend, definition: FlowDefinition): Promise<void> {
|
async add(flow: Flow, pubsub: PubSubBackend, definition: FlowDefinition): Promise<void> {
|
||||||
const topic = definition.topics?.[this.name] ?? this.name;
|
await flow.runInCompatibilityScope(this.addEffect(flow, definition), pubsub);
|
||||||
const producer = new Producer<T>(pubsub, topic);
|
|
||||||
await producer.start();
|
|
||||||
flow.registerProducer(this.name, producer as Producer<unknown>);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,30 +7,46 @@
|
||||||
* Python reference: trustgraph-base/trustgraph/base/prompt_client_spec.py
|
* Python reference: trustgraph-base/trustgraph/base/prompt_client_spec.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Effect } from "effect";
|
||||||
import type { Spec } from "./types.js";
|
import type { Spec } from "./types.js";
|
||||||
import type { PubSubBackend } from "../backend/types.js";
|
import type { PubSubBackend } from "../backend/types.js";
|
||||||
import type { Flow, FlowDefinition } from "../processor/flow.js";
|
import type { Flow, FlowDefinition } from "../processor/flow.js";
|
||||||
import { RequestResponse } from "../messaging/request-response.js";
|
import {
|
||||||
|
RequestResponseFactory,
|
||||||
|
type EffectRequestResponse,
|
||||||
|
} from "../messaging/runtime.js";
|
||||||
|
|
||||||
export class RequestResponseSpec<TReq, TRes> implements Spec {
|
export class RequestResponseSpec<TReq, TRes> implements Spec {
|
||||||
|
public readonly name: string;
|
||||||
|
private readonly requestTopicName: string;
|
||||||
|
private readonly responseTopicName: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
name: string,
|
||||||
private readonly requestTopicName: string,
|
requestTopicName: string,
|
||||||
private readonly responseTopicName: string,
|
responseTopicName: string,
|
||||||
) {}
|
) {
|
||||||
|
this.name = name;
|
||||||
|
this.requestTopicName = requestTopicName;
|
||||||
|
this.responseTopicName = responseTopicName;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEffect(flow: Flow, definition: FlowDefinition) {
|
||||||
|
const spec = this;
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const requestTopic = definition.topics?.[spec.requestTopicName] ?? spec.requestTopicName;
|
||||||
|
const responseTopic = definition.topics?.[spec.responseTopicName] ?? spec.responseTopicName;
|
||||||
|
const factory = yield* RequestResponseFactory;
|
||||||
|
const requestor = yield* factory.make<TReq, TRes>({
|
||||||
|
requestTopic,
|
||||||
|
responseTopic,
|
||||||
|
subscription: `${flow.processorId}-${flow.name}-${spec.name}`,
|
||||||
|
});
|
||||||
|
flow.registerRequestor(spec.name, requestor as EffectRequestResponse<unknown, unknown>);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async add(flow: Flow, pubsub: PubSubBackend, definition: FlowDefinition): Promise<void> {
|
async add(flow: Flow, pubsub: PubSubBackend, definition: FlowDefinition): Promise<void> {
|
||||||
const requestTopic = definition.topics?.[this.requestTopicName] ?? this.requestTopicName;
|
await flow.runInCompatibilityScope(this.addEffect(flow, definition), pubsub);
|
||||||
const responseTopic = definition.topics?.[this.responseTopicName] ?? this.responseTopicName;
|
|
||||||
|
|
||||||
const rr = new RequestResponse<TReq, TRes>({
|
|
||||||
pubsub,
|
|
||||||
requestTopic,
|
|
||||||
responseTopic,
|
|
||||||
subscription: `${flow.processorId}-${flow.name}-${this.name}`,
|
|
||||||
});
|
|
||||||
await rr.start();
|
|
||||||
|
|
||||||
flow.registerRequestor(this.name, rr as RequestResponse<unknown, unknown>);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,29 @@
|
||||||
* Python reference: trustgraph-base/trustgraph/base/spec.py and siblings
|
* Python reference: trustgraph-base/trustgraph/base/spec.py and siblings
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Effect, Scope } from "effect";
|
||||||
import type { PubSubBackend } from "../backend/types.js";
|
import type { PubSubBackend } from "../backend/types.js";
|
||||||
|
import type {
|
||||||
|
ConsumerFactory,
|
||||||
|
ProducerFactory,
|
||||||
|
RequestResponseFactory,
|
||||||
|
} from "../messaging/runtime.js";
|
||||||
import type { Flow, FlowDefinition } from "../processor/flow.js";
|
import type { Flow, FlowDefinition } from "../processor/flow.js";
|
||||||
|
import type { PubSubError } from "../errors.js";
|
||||||
|
|
||||||
export interface Spec {
|
export type SpecRuntimeRequirements =
|
||||||
|
| Scope.Scope
|
||||||
|
| ProducerFactory
|
||||||
|
| ConsumerFactory
|
||||||
|
| RequestResponseFactory;
|
||||||
|
|
||||||
|
export type SpecRuntimeError = PubSubError;
|
||||||
|
|
||||||
|
export interface Spec<Requirements = never> {
|
||||||
name: string;
|
name: string;
|
||||||
|
addEffect(
|
||||||
|
flow: Flow<Requirements>,
|
||||||
|
definition: FlowDefinition,
|
||||||
|
): Effect.Effect<void, SpecRuntimeError, SpecRuntimeRequirements | Requirements>;
|
||||||
add(flow: Flow, pubsub: PubSubBackend, definition: FlowDefinition): Promise<void>;
|
add(flow: Flow, pubsub: PubSubBackend, definition: FlowDefinition): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
|
"types": ["node"],
|
||||||
"composite": true
|
"composite": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
|
include: ["src/**/*.test.ts", "src/**/*.spec.ts"],
|
||||||
|
exclude: ["dist/**", "node_modules/**"],
|
||||||
globals: true,
|
globals: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,22 @@
|
||||||
"tg": "dist/index.js"
|
"tg": "dist/index.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "bunx --bun tsc",
|
||||||
"dev": "tsc --watch",
|
"dev": "tsc --watch",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"test": "vitest run --passWithNoTests"
|
"test": "bunx --bun vitest run --passWithNoTests"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trustgraph/base": "workspace:*",
|
"@trustgraph/base": "workspace:*",
|
||||||
"@trustgraph/client": "workspace:*",
|
"@trustgraph/client": "workspace:*",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
|
"effect": "4.0.0-beta.65",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
"@types/ws": "^8.5.0",
|
"@types/ws": "^8.5.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^3.1.0"
|
"vitest": "^4.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,15 @@ export function registerAgentCommands(program: Command): void {
|
||||||
question,
|
question,
|
||||||
(chunk) => {
|
(chunk) => {
|
||||||
// think — show thought process
|
// think — show thought process
|
||||||
if (chunk) process.stderr.write(chunk);
|
if (chunk.length > 0) process.stderr.write(chunk);
|
||||||
},
|
},
|
||||||
(chunk) => {
|
(chunk) => {
|
||||||
// observe — show observations
|
// observe — show observations
|
||||||
if (chunk) process.stderr.write(chunk);
|
if (chunk.length > 0) process.stderr.write(chunk);
|
||||||
},
|
},
|
||||||
(chunk, complete) => {
|
(chunk, complete) => {
|
||||||
// answer — print to stdout
|
// answer — print to stdout
|
||||||
if (chunk) process.stdout.write(chunk);
|
if (chunk.length > 0) process.stdout.write(chunk);
|
||||||
if (complete) {
|
if (complete) {
|
||||||
process.stdout.write("\n");
|
process.stdout.write("\n");
|
||||||
resolve();
|
resolve();
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,9 @@ export function registerFlowCommands(program: Command): void {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const flows = socket.flows();
|
const flows = socket.flows();
|
||||||
const params = cmdOpts.parameters
|
const rawParameters = cmdOpts.parameters as string | undefined;
|
||||||
? JSON.parse(cmdOpts.parameters as string)
|
const params = rawParameters !== undefined && rawParameters.length > 0
|
||||||
|
? JSON.parse(rawParameters)
|
||||||
: undefined;
|
: undefined;
|
||||||
const resp = await flows.startFlow(
|
const resp = await flows.startFlow(
|
||||||
id,
|
id,
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,14 @@ export function registerGraphRagCommands(program: Command): void {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const flow = socket.flow(opts.flow);
|
const flow = socket.flow(opts.flow);
|
||||||
|
const collection = cmdOpts.collection as string | undefined;
|
||||||
const response = await flow.graphRag(
|
const response = await flow.graphRag(
|
||||||
query,
|
query,
|
||||||
{
|
{
|
||||||
entityLimit: parseInt(cmdOpts.entityLimit, 10),
|
entityLimit: parseInt(cmdOpts.entityLimit, 10),
|
||||||
tripleLimit: parseInt(cmdOpts.tripleLimit, 10),
|
tripleLimit: parseInt(cmdOpts.tripleLimit, 10),
|
||||||
},
|
},
|
||||||
cmdOpts.collection,
|
collection,
|
||||||
);
|
);
|
||||||
console.log(response);
|
console.log(response);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -47,10 +48,14 @@ export function registerGraphRagCommands(program: Command): void {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const flow = socket.flow(opts.flow);
|
const flow = socket.flow(opts.flow);
|
||||||
|
const docLimit = cmdOpts.docLimit as string | undefined;
|
||||||
|
const collection = cmdOpts.collection as string | undefined;
|
||||||
const response = await flow.documentRag(
|
const response = await flow.documentRag(
|
||||||
query,
|
query,
|
||||||
cmdOpts.docLimit ? parseInt(cmdOpts.docLimit, 10) : undefined,
|
docLimit !== undefined && docLimit.length > 0
|
||||||
cmdOpts.collection,
|
? parseInt(docLimit, 10)
|
||||||
|
: undefined,
|
||||||
|
collection,
|
||||||
);
|
);
|
||||||
console.log(response);
|
console.log(response);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,15 @@
|
||||||
* Manages documents stored in the TrustGraph library.
|
* Manages documents stored in the TrustGraph library.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync } from "node:fs";
|
|
||||||
import { basename } from "node:path";
|
|
||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { createSocket, getOpts } from "./util.js";
|
import { createSocket, getOpts } from "./util.js";
|
||||||
|
|
||||||
|
function basenamePath(filepath: string): string {
|
||||||
|
const normalized = filepath.replace(/\/+$/, "");
|
||||||
|
const index = normalized.lastIndexOf("/");
|
||||||
|
return index >= 0 ? normalized.slice(index + 1) : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
/** Simple MIME-type lookup by file extension. */
|
/** Simple MIME-type lookup by file extension. */
|
||||||
function guessMimeType(filepath: string): string {
|
function guessMimeType(filepath: string): string {
|
||||||
const ext = filepath.split(".").pop()?.toLowerCase();
|
const ext = filepath.split(".").pop()?.toLowerCase();
|
||||||
|
|
@ -69,10 +73,10 @@ export function registerLibraryCommands(program: Command): void {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const lib = socket.librarian();
|
const lib = socket.librarian();
|
||||||
const data = readFileSync(file);
|
const data = new Uint8Array(await Bun.file(file).arrayBuffer());
|
||||||
const b64 = data.toString("base64");
|
const b64 = Buffer.from(data).toString("base64");
|
||||||
const mimeType = (cmdOpts.mimeType as string | undefined) ?? guessMimeType(file);
|
const mimeType = (cmdOpts.mimeType as string | undefined) ?? guessMimeType(file);
|
||||||
const title = (cmdOpts.title as string | undefined) ?? basename(file);
|
const title = (cmdOpts.title as string | undefined) ?? basenamePath(file);
|
||||||
const comments = cmdOpts.comments as string;
|
const comments = cmdOpts.comments as string;
|
||||||
const tags: string[] = (cmdOpts.tags as string[] | undefined) ?? [];
|
const tags: string[] = (cmdOpts.tags as string[] | undefined) ?? [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,17 @@ export function registerTriplesCommands(program: Command): void {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const flow = socket.flow(opts.flow);
|
const flow = socket.flow(opts.flow);
|
||||||
const s: Term | undefined = cmdOpts.subject
|
const subject = cmdOpts.subject as string | undefined;
|
||||||
? { t: "i", i: cmdOpts.subject as string }
|
const predicate = cmdOpts.predicate as string | undefined;
|
||||||
|
const object = cmdOpts.object as string | undefined;
|
||||||
|
const s: Term | undefined = subject !== undefined && subject.length > 0
|
||||||
|
? { t: "i", i: subject }
|
||||||
: undefined;
|
: undefined;
|
||||||
const p: Term | undefined = cmdOpts.predicate
|
const p: Term | undefined = predicate !== undefined && predicate.length > 0
|
||||||
? { t: "i", i: cmdOpts.predicate as string }
|
? { t: "i", i: predicate }
|
||||||
: undefined;
|
: undefined;
|
||||||
const o: Term | undefined = cmdOpts.object
|
const o: Term | undefined = object !== undefined && object.length > 0
|
||||||
? { t: "i", i: cmdOpts.object as string }
|
? { t: "i", i: object }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const triples = await flow.triplesQuery(
|
const triples = await flow.triplesQuery(
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export interface CliOpts {
|
||||||
export function getOpts(cmd: Command): CliOpts {
|
export function getOpts(cmd: Command): CliOpts {
|
||||||
// Walk up to root command to get global options
|
// Walk up to root command to get global options
|
||||||
let root = cmd;
|
let root = cmd;
|
||||||
while (root.parent) root = root.parent;
|
while (root.parent !== null) root = root.parent;
|
||||||
return root.opts() as CliOpts;
|
return root.opts() as CliOpts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
|
"types": ["node", "bun"],
|
||||||
"composite": true
|
"composite": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,13 @@
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "bunx --bun tsc",
|
||||||
"dev": "tsc --watch",
|
"dev": "tsc --watch",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"test": "vitest run"
|
"test": "bunx --bun vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"effect": "4.0.0-beta.65"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"ws": "^8.0.0"
|
"ws": "^8.0.0"
|
||||||
|
|
@ -20,10 +23,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/ws": "^8.5.0",
|
"@types/ws": "^8.5.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^3.1.0",
|
"vitest": "^4.1.6",
|
||||||
"happy-dom": "^20.0.0"
|
"happy-dom": "^20.0.0"
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Triple, Term } from "./Triple.js";
|
import type { Term, Triple } from "./Triple.js";
|
||||||
|
|
||||||
export type Request = object;
|
export type Request = object;
|
||||||
export type Response = object;
|
export type Response = object;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { RequestMessage } from "../models/messages.js";
|
import type { RequestMessage } from "../models/messages.js";
|
||||||
import { WS_OPEN, WS_CONNECTING, type IsomorphicWebSocket } from "./websocket-adapter.js";
|
import { WS_OPEN, WS_CONNECTING, type IsomorphicWebSocket } from "./websocket-adapter.js";
|
||||||
|
|
||||||
// Constant defining the delay before attempting to reconnect a WebSocket
|
// Constant defining the delay before attempting to reconnect a WebSocket
|
||||||
|
|
@ -8,8 +8,14 @@ export const SOCKET_RECONNECTION_TIMEOUT = 2000;
|
||||||
// Forward declare Socket type to avoid circular dependency
|
// Forward declare Socket type to avoid circular dependency
|
||||||
// Using a minimal interface that matches what BaseApi provides
|
// Using a minimal interface that matches what BaseApi provides
|
||||||
interface Socket {
|
interface Socket {
|
||||||
ws?: IsomorphicWebSocket;
|
ws: IsomorphicWebSocket | null | undefined;
|
||||||
inflight: { [key: string]: ServiceCallMulti };
|
inflight: {
|
||||||
|
[key: string]: {
|
||||||
|
onReceived: (resp: object) => void;
|
||||||
|
retryNow: () => void;
|
||||||
|
error: (err: object | string) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
reopen: () => void;
|
reopen: () => void;
|
||||||
getNextId?: () => string;
|
getNextId?: () => string;
|
||||||
user?: string;
|
user?: string;
|
||||||
|
|
@ -42,7 +48,7 @@ export class ServiceCallMulti {
|
||||||
success: (resp: unknown) => void;
|
success: (resp: unknown) => void;
|
||||||
error: (err: object | string) => void;
|
error: (err: object | string) => void;
|
||||||
receiver: (resp: unknown) => boolean;
|
receiver: (resp: unknown) => boolean;
|
||||||
timeoutId?: ReturnType<typeof setTimeout>;
|
timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
retries: number;
|
retries: number;
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
|
|
@ -121,7 +127,7 @@ export class ServiceCallMulti {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if WebSocket connection is available and ready
|
// Check if WebSocket connection is available and ready
|
||||||
if (this.socket.ws && this.socket.ws.readyState === WS_OPEN) {
|
if (this.socket.ws !== null && this.socket.ws !== undefined && this.socket.ws.readyState === WS_OPEN) {
|
||||||
try {
|
try {
|
||||||
this.socket.ws.send(JSON.stringify(this.msg));
|
this.socket.ws.send(JSON.stringify(this.msg));
|
||||||
this.timeoutId = setTimeout(this.onTimeout.bind(this), this.timeout);
|
this.timeoutId = setTimeout(this.onTimeout.bind(this), this.timeout);
|
||||||
|
|
@ -148,7 +154,8 @@ export class ServiceCallMulti {
|
||||||
// No WebSocket connection available or not ready
|
// No WebSocket connection available or not ready
|
||||||
// Check if socket is connecting
|
// Check if socket is connecting
|
||||||
if (
|
if (
|
||||||
this.socket.ws &&
|
this.socket.ws !== null &&
|
||||||
|
this.socket.ws !== undefined &&
|
||||||
this.socket.ws.readyState === WS_CONNECTING
|
this.socket.ws.readyState === WS_CONNECTING
|
||||||
) {
|
) {
|
||||||
// Wait a bit longer for connection to establish
|
// Wait a bit longer for connection to establish
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { RequestMessage } from "../models/messages.js";
|
import type { RequestMessage } from "../models/messages.js";
|
||||||
import { WS_OPEN, type IsomorphicWebSocket } from "./websocket-adapter.js";
|
import { WS_OPEN, type IsomorphicWebSocket } from "./websocket-adapter.js";
|
||||||
|
|
||||||
// Constant defining the delay before attempting to reconnect a WebSocket
|
// Constant defining the delay before attempting to reconnect a WebSocket
|
||||||
|
|
@ -8,8 +8,14 @@ export const SOCKET_RECONNECTION_TIMEOUT = 2000;
|
||||||
// Forward declare Socket type to avoid circular dependency
|
// Forward declare Socket type to avoid circular dependency
|
||||||
// Using a minimal interface that matches what BaseApi provides
|
// Using a minimal interface that matches what BaseApi provides
|
||||||
interface Socket {
|
interface Socket {
|
||||||
ws?: IsomorphicWebSocket;
|
ws: IsomorphicWebSocket | null | undefined;
|
||||||
inflight: { [key: string]: ServiceCall };
|
inflight: {
|
||||||
|
[key: string]: {
|
||||||
|
onReceived: (resp: object) => void;
|
||||||
|
retryNow: () => void;
|
||||||
|
error: (err: object | string) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
reopen: () => void;
|
reopen: () => void;
|
||||||
getNextId?: () => string;
|
getNextId?: () => string;
|
||||||
user?: string;
|
user?: string;
|
||||||
|
|
@ -52,7 +58,7 @@ export class ServiceCall {
|
||||||
msg: RequestMessage; // The request message
|
msg: RequestMessage; // The request message
|
||||||
success: (resp: unknown) => void; // Success callback
|
success: (resp: unknown) => void; // Success callback
|
||||||
error: (err: object | string) => void; // Error callback
|
error: (err: object | string) => void; // Error callback
|
||||||
timeoutId?: ReturnType<typeof setTimeout>; // Reference to the active timeout timer
|
timeoutId: ReturnType<typeof setTimeout> | undefined = undefined; // Reference to the active timeout timer
|
||||||
timeout: number; // Timeout duration in milliseconds
|
timeout: number; // Timeout duration in milliseconds
|
||||||
retries: number; // Remaining retry attempts
|
retries: number; // Remaining retry attempts
|
||||||
socket: Socket; // WebSocket connection reference
|
socket: Socket; // WebSocket connection reference
|
||||||
|
|
@ -77,7 +83,10 @@ export class ServiceCall {
|
||||||
*/
|
*/
|
||||||
onReceived(resp: object) {
|
onReceived(resp: object) {
|
||||||
// Guard: ignore duplicate responses after completion
|
// Guard: ignore duplicate responses after completion
|
||||||
if (this.complete) return;
|
if (this.complete) {
|
||||||
|
console.log(this.mid, "should not happen, request is already complete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Mark as complete to prevent duplicate processing
|
// Mark as complete to prevent duplicate processing
|
||||||
this.complete = true;
|
this.complete = true;
|
||||||
|
|
@ -93,18 +102,18 @@ export class ServiceCall {
|
||||||
let errorToHandle: unknown = null;
|
let errorToHandle: unknown = null;
|
||||||
|
|
||||||
// Check for direct error in response
|
// Check for direct error in response
|
||||||
if (resp && typeof resp === "object" && "error" in resp) {
|
if (resp !== null && typeof resp === "object" && "error" in resp) {
|
||||||
errorToHandle = (resp as Record<string, unknown>).error;
|
errorToHandle = (resp as Record<string, unknown>).error;
|
||||||
}
|
}
|
||||||
// Check for nested error under response property
|
// Check for nested error under response property
|
||||||
else if (resp && typeof resp === "object" && "response" in resp) {
|
else if (resp !== null && typeof resp === "object" && "response" in resp) {
|
||||||
const response = (resp as Record<string, unknown>).response;
|
const response = (resp as Record<string, unknown>).response;
|
||||||
if (response && typeof response === "object" && "error" in response) {
|
if (response !== null && typeof response === "object" && "error" in response) {
|
||||||
errorToHandle = (response as Record<string, unknown>).error;
|
errorToHandle = (response as Record<string, unknown>).error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorToHandle) {
|
if (errorToHandle !== null && errorToHandle !== undefined) {
|
||||||
// Response contains an error - call error callback
|
// Response contains an error - call error callback
|
||||||
const errorObj = errorToHandle as Record<string, unknown>;
|
const errorObj = errorToHandle as Record<string, unknown>;
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
|
|
@ -151,7 +160,13 @@ export class ServiceCall {
|
||||||
*/
|
*/
|
||||||
onTimeout() {
|
onTimeout() {
|
||||||
// Guard: ignore timeout after completion
|
// Guard: ignore timeout after completion
|
||||||
if (this.complete) return;
|
if (this.complete) {
|
||||||
|
console.log(
|
||||||
|
this.mid,
|
||||||
|
"timeout should not happen, request is already complete",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Request", this.mid, "timed out");
|
console.log("Request", this.mid, "timed out");
|
||||||
|
|
||||||
|
|
@ -180,7 +195,13 @@ export class ServiceCall {
|
||||||
*/
|
*/
|
||||||
attempt() {
|
attempt() {
|
||||||
// Guard: don't retry completed requests
|
// Guard: don't retry completed requests
|
||||||
if (this.complete) return;
|
if (this.complete) {
|
||||||
|
console.log(
|
||||||
|
this.mid,
|
||||||
|
"attempt should not be called, request is already complete",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Decrement retry counter
|
// Decrement retry counter
|
||||||
this.retries--;
|
this.retries--;
|
||||||
|
|
@ -197,7 +218,7 @@ export class ServiceCall {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if WebSocket connection is available and ready
|
// Check if WebSocket connection is available and ready
|
||||||
if (this.socket.ws && this.socket.ws.readyState === WS_OPEN) {
|
if (this.socket.ws !== null && this.socket.ws !== undefined && this.socket.ws.readyState === WS_OPEN) {
|
||||||
try {
|
try {
|
||||||
// Attempt to send the message as JSON
|
// Attempt to send the message as JSON
|
||||||
this.socket.ws.send(JSON.stringify(this.msg));
|
this.socket.ws.send(JSON.stringify(this.msg));
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -117,7 +117,9 @@ export function getDefaultSocketUrl(): string {
|
||||||
*/
|
*/
|
||||||
export function getRandomValues(array: Uint32Array): Uint32Array {
|
export function getRandomValues(array: Uint32Array): Uint32Array {
|
||||||
if (typeof globalThis.crypto?.getRandomValues === "function") {
|
if (typeof globalThis.crypto?.getRandomValues === "function") {
|
||||||
return globalThis.crypto.getRandomValues(array);
|
const random = globalThis.crypto.getRandomValues(new Uint32Array(array.length));
|
||||||
|
array.set(random);
|
||||||
|
return array;
|
||||||
}
|
}
|
||||||
// Node.js fallback for versions < 19 where globalThis.crypto may not exist
|
// Node.js fallback for versions < 19 where globalThis.crypto may not exist
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"lib": ["ES2022", "DOM"],
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"types": ["node"],
|
||||||
"composite": true
|
"composite": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "bunx --bun tsc",
|
||||||
"dev": "tsc --watch",
|
"dev": "tsc --watch",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"test": "vitest run"
|
"test": "bunx --bun vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.39.0",
|
"@anthropic-ai/sdk": "^0.39.0",
|
||||||
|
"@effect/platform-bun": "4.0.0-beta.65",
|
||||||
"@fastify/websocket": "^11.0.0",
|
"@fastify/websocket": "^11.0.0",
|
||||||
"@qdrant/js-client-rest": "^1.13.0",
|
"@qdrant/js-client-rest": "^1.13.0",
|
||||||
"@trustgraph/base": "workspace:*",
|
"@trustgraph/base": "workspace:*",
|
||||||
|
|
@ -20,12 +21,14 @@
|
||||||
"ollama": "^0.6.3",
|
"ollama": "^0.6.3",
|
||||||
"@mistralai/mistralai": "^1.0.0",
|
"@mistralai/mistralai": "^1.0.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||||
|
"effect": "4.0.0-beta.65",
|
||||||
"openai": "^4.85.0",
|
"openai": "^4.85.0",
|
||||||
"pdfjs-dist": "^5.6.205"
|
"pdfjs-dist": "^5.6.205"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/vitest": "4.0.0-beta.65",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^3.1.0"
|
"vitest": "^4.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
230
ts/packages/flow/src/__tests__/chunking-service.test.ts
Normal file
230
ts/packages/flow/src/__tests__/chunking-service.test.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
import { describe, expect, it } from "@effect/vitest";
|
||||||
|
import { ConfigProvider, Effect, Fiber } from "effect";
|
||||||
|
import {
|
||||||
|
MessagingRuntimeLive,
|
||||||
|
PubSub,
|
||||||
|
runProcessorScoped,
|
||||||
|
topics,
|
||||||
|
type BackendConsumer,
|
||||||
|
type BackendProducer,
|
||||||
|
type Chunk,
|
||||||
|
type CreateConsumerOptions,
|
||||||
|
type CreateProducerOptions,
|
||||||
|
type Message,
|
||||||
|
type PubSubBackend,
|
||||||
|
type TextDocument,
|
||||||
|
} from "@trustgraph/base";
|
||||||
|
import { ChunkingService } from "../chunking/service.js";
|
||||||
|
import { recursiveSplit } from "../chunking/recursive-splitter.js";
|
||||||
|
|
||||||
|
function createMessage<T>(value: T, properties: Record<string, string> = {}): Message<T> {
|
||||||
|
return {
|
||||||
|
value: () => value,
|
||||||
|
properties: () => properties,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitFor = (condition: () => boolean, label: string) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
const deadline = Date.now() + 1000;
|
||||||
|
const check = () => {
|
||||||
|
if (condition()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Date.now() > deadline) {
|
||||||
|
reject(new Error(`Timed out waiting for ${label}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(check, 5);
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
}),
|
||||||
|
catch: (error) => error,
|
||||||
|
});
|
||||||
|
|
||||||
|
class RecordingProducer<T> implements BackendProducer<T> {
|
||||||
|
readonly sent: Array<{ readonly message: T; readonly properties?: Record<string, string> }> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
flushCount = 0;
|
||||||
|
|
||||||
|
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||||
|
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||||
|
}
|
||||||
|
|
||||||
|
async flush(): Promise<void> {
|
||||||
|
this.flushCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PushConsumer<T> implements BackendConsumer<T> {
|
||||||
|
readonly acknowledged: Array<Message<T>> = [];
|
||||||
|
readonly nacked: Array<Message<T>> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
private readonly messages: Array<Message<T>> = [];
|
||||||
|
private readonly waiters: Array<(message: Message<T> | null) => void> = [];
|
||||||
|
private closed = false;
|
||||||
|
|
||||||
|
push(message: Message<T>): void {
|
||||||
|
const waiter = this.waiters.shift();
|
||||||
|
if (waiter !== undefined) {
|
||||||
|
waiter(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async receive(): Promise<Message<T> | null> {
|
||||||
|
const message = this.messages.shift();
|
||||||
|
if (message !== undefined || this.closed) {
|
||||||
|
return message ?? null;
|
||||||
|
}
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
this.waiters.push(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async acknowledge(message: Message<T>): Promise<void> {
|
||||||
|
this.acknowledged.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async negativeAcknowledge(message: Message<T>): Promise<void> {
|
||||||
|
this.nacked.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribe(): Promise<void> {}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closed = true;
|
||||||
|
for (const waiter of this.waiters.splice(0)) {
|
||||||
|
waiter(null);
|
||||||
|
}
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChunkingBackend implements PubSubBackend {
|
||||||
|
readonly configConsumer = new PushConsumer<{ readonly version: number; readonly config: Record<string, unknown> }>();
|
||||||
|
readonly consumersByTopic = new Map<string, PushConsumer<unknown>>();
|
||||||
|
readonly producersByTopic = new Map<string, RecordingProducer<unknown>>();
|
||||||
|
readonly producerOptions: Array<CreateProducerOptions> = [];
|
||||||
|
readonly consumerOptions: Array<CreateConsumerOptions> = [];
|
||||||
|
closeCount = 0;
|
||||||
|
|
||||||
|
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||||
|
this.producerOptions.push(options);
|
||||||
|
const producer = new RecordingProducer<unknown>();
|
||||||
|
this.producersByTopic.set(options.topic, producer);
|
||||||
|
return producer as BackendProducer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||||
|
this.consumerOptions.push(options);
|
||||||
|
if (options.topic === topics.configPush) {
|
||||||
|
return this.configConsumer as unknown as BackendConsumer<T>;
|
||||||
|
}
|
||||||
|
const consumer = new PushConsumer<unknown>();
|
||||||
|
this.consumersByTopic.set(options.topic, consumer);
|
||||||
|
return consumer as BackendConsumer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.closeCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushConfig(): void {
|
||||||
|
this.configConsumer.push(
|
||||||
|
createMessage({
|
||||||
|
version: 1,
|
||||||
|
config: {
|
||||||
|
flows: {
|
||||||
|
default: {
|
||||||
|
topics: {
|
||||||
|
"chunk-input": "chunk-input-topic",
|
||||||
|
"chunk-output": "chunk-output-topic",
|
||||||
|
"chunk-triples": "chunk-triples-topic",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
"chunk-size": 18,
|
||||||
|
"chunk-overlap": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fastMessagingConfig = ConfigProvider.layer(
|
||||||
|
ConfigProvider.fromEnv({
|
||||||
|
TG_CONSUMER_RECEIVE_TIMEOUT_MS: "1",
|
||||||
|
TG_CONSUMER_ERROR_BACKOFF_MS: "1",
|
||||||
|
TG_RATE_LIMIT_RETRY_MS: "1",
|
||||||
|
TG_REQUEST_TIMEOUT_MS: "250",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("ChunkingService", () => {
|
||||||
|
it.effect(
|
||||||
|
"handles chunk-input with native Effect flow resources",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const backend = new ChunkingBackend();
|
||||||
|
|
||||||
|
yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fiber = yield* runProcessorScoped(
|
||||||
|
{
|
||||||
|
id: "chunking",
|
||||||
|
pubsubUrl: "nats://unused:4222",
|
||||||
|
metricsPort: 8000,
|
||||||
|
manageProcessSignals: true,
|
||||||
|
},
|
||||||
|
(config) => new ChunkingService(config),
|
||||||
|
).pipe(
|
||||||
|
Effect.provide(MessagingRuntimeLive),
|
||||||
|
Effect.provide(PubSub.layer(backend)),
|
||||||
|
Effect.provide(fastMessagingConfig),
|
||||||
|
Effect.forkChild,
|
||||||
|
);
|
||||||
|
|
||||||
|
backend.pushConfig();
|
||||||
|
yield* waitFor(() => backend.consumersByTopic.has("chunk-input-topic"), "chunk consumer");
|
||||||
|
yield* waitFor(() => backend.producersByTopic.has("chunk-output-topic"), "chunk producer");
|
||||||
|
|
||||||
|
const document: TextDocument = {
|
||||||
|
documentId: "doc-1",
|
||||||
|
metadata: {
|
||||||
|
id: "pipeline-1",
|
||||||
|
root: "root-1",
|
||||||
|
user: "user-1",
|
||||||
|
collection: "collection-1",
|
||||||
|
},
|
||||||
|
text: "alpha beta gamma delta epsilon zeta eta theta",
|
||||||
|
};
|
||||||
|
const inputConsumer = backend.consumersByTopic.get("chunk-input-topic") as PushConsumer<TextDocument>;
|
||||||
|
inputConsumer.push(createMessage(document, { id: "request-1" }));
|
||||||
|
|
||||||
|
const outputProducer = backend.producersByTopic.get("chunk-output-topic") as RecordingProducer<Chunk>;
|
||||||
|
const expectedChunks = recursiveSplit(document.text, 18, 0);
|
||||||
|
yield* waitFor(() => outputProducer.sent.length === expectedChunks.length, "chunk outputs");
|
||||||
|
|
||||||
|
expect(inputConsumer.acknowledged.length).toBe(1);
|
||||||
|
expect(inputConsumer.nacked).toEqual([]);
|
||||||
|
expect(outputProducer.sent.map(({ message }) => message.chunk)).toEqual(expectedChunks);
|
||||||
|
expect(outputProducer.sent.every(({ properties }) => properties?.id === "request-1")).toBe(true);
|
||||||
|
|
||||||
|
yield* Fiber.interrupt(fiber);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(backend.closeCount).toBe(1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
82
ts/packages/flow/src/__tests__/ollama-embeddings.test.ts
Normal file
82
ts/packages/flow/src/__tests__/ollama-embeddings.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { describe, expect, it } from "@effect/vitest";
|
||||||
|
import { Effect } from "effect";
|
||||||
|
import { makeOllamaEmbeddings } from "../embeddings/ollama.js";
|
||||||
|
|
||||||
|
describe("Ollama embeddings provider", () => {
|
||||||
|
it.effect(
|
||||||
|
"posts embedding requests to Ollama",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const calls: Array<{ readonly input: RequestInfo | URL; readonly init?: RequestInit }> = [];
|
||||||
|
const fetchImpl = ((input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
calls.push(init === undefined ? { input } : { input, init });
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(JSON.stringify({ embeddings: [[1, 2, 3]] }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}) as typeof fetch;
|
||||||
|
const embeddings = makeOllamaEmbeddings({
|
||||||
|
id: "embeddings",
|
||||||
|
model: "default-model",
|
||||||
|
ollamaHost: "http://ollama.local",
|
||||||
|
fetch: fetchImpl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const vectors = yield* embeddings.embed(["alpha"], "override-model");
|
||||||
|
|
||||||
|
expect(vectors).toEqual([[1, 2, 3]]);
|
||||||
|
expect(calls).toHaveLength(1);
|
||||||
|
expect(String(calls[0]?.input)).toBe("http://ollama.local/api/embed");
|
||||||
|
expect(calls[0]?.init?.method).toBe("POST");
|
||||||
|
expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({
|
||||||
|
model: "override-model",
|
||||||
|
input: ["alpha"],
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"does not call Ollama for empty requests",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const calls: Array<RequestInfo | URL> = [];
|
||||||
|
const fetchImpl = ((input: RequestInfo | URL) => {
|
||||||
|
calls.push(input);
|
||||||
|
return Promise.resolve(new Response(JSON.stringify({ embeddings: [] })));
|
||||||
|
}) as typeof fetch;
|
||||||
|
const embeddings = makeOllamaEmbeddings({
|
||||||
|
id: "embeddings",
|
||||||
|
fetch: fetchImpl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const vectors = yield* embeddings.embed([]);
|
||||||
|
|
||||||
|
expect(vectors).toEqual([]);
|
||||||
|
expect(calls).toEqual([]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"maps failed Ollama responses to EmbeddingsError",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const fetchImpl = (() =>
|
||||||
|
Promise.resolve(
|
||||||
|
new Response("not found", {
|
||||||
|
status: 404,
|
||||||
|
}),
|
||||||
|
)) as typeof fetch;
|
||||||
|
const embeddings = makeOllamaEmbeddings({
|
||||||
|
id: "embeddings",
|
||||||
|
ollamaHost: "http://ollama.local",
|
||||||
|
fetch: fetchImpl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = yield* embeddings.embed(["alpha"]).pipe(Effect.flip);
|
||||||
|
|
||||||
|
expect(error._tag).toBe("EmbeddingsError");
|
||||||
|
expect(error.operation).toBe("ollama.embed");
|
||||||
|
expect(error.provider).toBe("ollama");
|
||||||
|
expect(error.message).toContain("Ollama embeddings request failed (404): not found");
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
type ToolRequest,
|
type ToolRequest,
|
||||||
type ToolResponse,
|
type ToolResponse,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
|
import { makeProcessorProgram } from "@trustgraph/base";
|
||||||
|
|
||||||
interface McpServiceConfig {
|
interface McpServiceConfig {
|
||||||
url: string;
|
url: string;
|
||||||
|
|
@ -36,7 +37,7 @@ export class McpToolService extends FlowProcessor {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
this.registerSpecification(
|
this.registerSpecification(
|
||||||
new ConsumerSpec<ToolRequest>("mcp-tool-request", this.onRequest.bind(this)),
|
ConsumerSpec.fromPromise<ToolRequest>("mcp-tool-request", this.onRequest.bind(this)),
|
||||||
);
|
);
|
||||||
this.registerSpecification(new ProducerSpec<ToolResponse>("mcp-tool-response"));
|
this.registerSpecification(new ProducerSpec<ToolResponse>("mcp-tool-response"));
|
||||||
|
|
||||||
|
|
@ -77,14 +78,16 @@ export class McpToolService extends FlowProcessor {
|
||||||
flowCtx: FlowContext,
|
flowCtx: FlowContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const requestId = properties.id;
|
const requestId = properties.id;
|
||||||
if (!requestId) return;
|
if (requestId === undefined || requestId.length === 0) return;
|
||||||
|
|
||||||
const responseProducer = flowCtx.flow.producer<ToolResponse>("mcp-tool-response");
|
const responseProducer = flowCtx.flow.producer<ToolResponse>("mcp-tool-response");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.invokeTool(
|
const result = await this.invokeTool(
|
||||||
msg.name,
|
msg.name,
|
||||||
msg.parameters ? JSON.parse(msg.parameters) : {},
|
msg.parameters !== undefined && msg.parameters.length > 0
|
||||||
|
? JSON.parse(msg.parameters) as Record<string, unknown>
|
||||||
|
: {},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (typeof result === "string") {
|
if (typeof result === "string") {
|
||||||
|
|
@ -110,7 +113,7 @@ export class McpToolService extends FlowProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
const svcConfig = this.mcpServices[name];
|
const svcConfig = this.mcpServices[name];
|
||||||
if (!svcConfig.url) {
|
if (svcConfig.url.length === 0) {
|
||||||
throw new Error(`MCP service "${name}" URL not defined`);
|
throw new Error(`MCP service "${name}" URL not defined`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,7 +121,7 @@ export class McpToolService extends FlowProcessor {
|
||||||
|
|
||||||
// Build headers with optional bearer token
|
// Build headers with optional bearer token
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (svcConfig["auth-token"]) {
|
if (svcConfig["auth-token"] !== undefined && svcConfig["auth-token"].length > 0) {
|
||||||
headers["Authorization"] = `Bearer ${svcConfig["auth-token"]}`;
|
headers["Authorization"] = `Bearer ${svcConfig["auth-token"]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,7 +136,7 @@ export class McpToolService extends FlowProcessor {
|
||||||
const client = new Client({ name: "trustgraph-mcp-client", version: "1.0.0" });
|
const client = new Client({ name: "trustgraph-mcp-client", version: "1.0.0" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.connect(transport);
|
await client.connect(transport as unknown as Parameters<Client["connect"]>[0]);
|
||||||
|
|
||||||
const result = await client.callTool({
|
const result = await client.callTool({
|
||||||
name: remoteName,
|
name: remoteName,
|
||||||
|
|
@ -141,11 +144,11 @@ export class McpToolService extends FlowProcessor {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract response — prefer structured content, fall back to text
|
// Extract response — prefer structured content, fall back to text
|
||||||
if (result.structuredContent) {
|
if (result.structuredContent !== undefined && result.structuredContent !== null) {
|
||||||
return result.structuredContent;
|
return result.structuredContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.content && Array.isArray(result.content)) {
|
if (result.content !== undefined && Array.isArray(result.content)) {
|
||||||
return result.content
|
return result.content
|
||||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||||
.map((c) => c.text)
|
.map((c) => c.text)
|
||||||
|
|
@ -158,3 +161,8 @@ export class McpToolService extends FlowProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const program = makeProcessorProgram({
|
||||||
|
id: "mcp-tool",
|
||||||
|
make: (config) => new McpToolService(config),
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,22 @@ const MAX_MARKER_LEN = Math.max(...MARKERS.map((m) => m.prefix.length));
|
||||||
export class StreamingReActParser {
|
export class StreamingReActParser {
|
||||||
private state: ReActState = "initial";
|
private state: ReActState = "initial";
|
||||||
private buffer = "";
|
private buffer = "";
|
||||||
|
private onThought: (text: string) => void;
|
||||||
|
private onAction: (name: string) => void;
|
||||||
|
private onActionInput: (input: string) => void;
|
||||||
|
private onFinalAnswer: (text: string) => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private onThought: (text: string) => void,
|
onThought: (text: string) => void,
|
||||||
private onAction: (name: string) => void,
|
onAction: (name: string) => void,
|
||||||
private onActionInput: (input: string) => void,
|
onActionInput: (input: string) => void,
|
||||||
private onFinalAnswer: (text: string) => void,
|
onFinalAnswer: (text: string) => void,
|
||||||
) {}
|
) {
|
||||||
|
this.onThought = onThought;
|
||||||
|
this.onAction = onAction;
|
||||||
|
this.onActionInput = onActionInput;
|
||||||
|
this.onFinalAnswer = onFinalAnswer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feed a chunk of LLM output text into the parser.
|
* Feed a chunk of LLM output text into the parser.
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import {
|
||||||
type ToolRequest,
|
type ToolRequest,
|
||||||
type ToolResponse,
|
type ToolResponse,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
|
import { makeProcessorProgram } from "@trustgraph/base";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createKnowledgeQueryTool,
|
createKnowledgeQueryTool,
|
||||||
|
|
@ -45,7 +46,7 @@ import {
|
||||||
type ExplainData,
|
type ExplainData,
|
||||||
} from "./tools.js";
|
} from "./tools.js";
|
||||||
import { buildReActPrompt } from "./prompt.js";
|
import { buildReActPrompt } from "./prompt.js";
|
||||||
import { filterToolsByGroupAndState, getNextState } from "../tool-filter.js";
|
import { filterToolsByGroupAndState } from "../tool-filter.js";
|
||||||
import type { AgentTool, ToolArg } from "./types.js";
|
import type { AgentTool, ToolArg } from "./types.js";
|
||||||
|
|
||||||
const MAX_ITERATIONS = 10;
|
const MAX_ITERATIONS = 10;
|
||||||
|
|
@ -59,7 +60,7 @@ export class AgentService extends FlowProcessor {
|
||||||
|
|
||||||
// Consumer: agent requests
|
// Consumer: agent requests
|
||||||
this.registerSpecification(
|
this.registerSpecification(
|
||||||
new ConsumerSpec<AgentRequest>("agent-request", this.onRequest.bind(this)),
|
ConsumerSpec.fromPromise<AgentRequest>("agent-request", this.onRequest.bind(this)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Producer: agent responses (streaming chunks)
|
// Producer: agent responses (streaming chunks)
|
||||||
|
|
@ -132,11 +133,12 @@ export class AgentService extends FlowProcessor {
|
||||||
for (const [_toolId, toolValue] of Object.entries(toolConfig)) {
|
for (const [_toolId, toolValue] of Object.entries(toolConfig)) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(toolValue) as Record<string, unknown>;
|
const data = JSON.parse(toolValue) as Record<string, unknown>;
|
||||||
const implType = data["type"] as string;
|
const implType = typeof data["type"] === "string" ? data["type"] : "";
|
||||||
const name = data["name"] as string;
|
const name = typeof data["name"] === "string" ? data["name"] : "";
|
||||||
const description = data["description"] as string ?? "";
|
const description =
|
||||||
|
typeof data["description"] === "string" ? data["description"] : "";
|
||||||
|
|
||||||
if (!name) {
|
if (name.length === 0) {
|
||||||
console.warn(`[AgentService] Skipping tool with no name: ${_toolId}`);
|
console.warn(`[AgentService] Skipping tool with no name: ${_toolId}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +150,10 @@ export class AgentService extends FlowProcessor {
|
||||||
// Will be wired to requestor at request time
|
// Will be wired to requestor at request time
|
||||||
tool = {
|
tool = {
|
||||||
name,
|
name,
|
||||||
description: description || "Query the knowledge graph for information about entities and their relationships.",
|
description:
|
||||||
|
description.length > 0
|
||||||
|
? description
|
||||||
|
: "Query the knowledge graph for information about entities and their relationships.",
|
||||||
args: [{ name: "question", type: "string", description: "The question to ask" }],
|
args: [{ name: "question", type: "string", description: "The question to ask" }],
|
||||||
config: data,
|
config: data,
|
||||||
execute: async () => "", // placeholder — wired at request time
|
execute: async () => "", // placeholder — wired at request time
|
||||||
|
|
@ -158,7 +163,10 @@ export class AgentService extends FlowProcessor {
|
||||||
case "document-query":
|
case "document-query":
|
||||||
tool = {
|
tool = {
|
||||||
name,
|
name,
|
||||||
description: description || "Search documents for relevant information.",
|
description:
|
||||||
|
description.length > 0
|
||||||
|
? description
|
||||||
|
: "Search documents for relevant information.",
|
||||||
args: [{ name: "question", type: "string", description: "The question to search for" }],
|
args: [{ name: "question", type: "string", description: "The question to search for" }],
|
||||||
config: data,
|
config: data,
|
||||||
execute: async () => "",
|
execute: async () => "",
|
||||||
|
|
@ -168,7 +176,10 @@ export class AgentService extends FlowProcessor {
|
||||||
case "triples-query":
|
case "triples-query":
|
||||||
tool = {
|
tool = {
|
||||||
name,
|
name,
|
||||||
description: description || "Query for specific triples in the knowledge graph.",
|
description:
|
||||||
|
description.length > 0
|
||||||
|
? description
|
||||||
|
: "Query for specific triples in the knowledge graph.",
|
||||||
args: [
|
args: [
|
||||||
{ name: "subject", type: "string", description: "Subject entity (optional)" },
|
{ name: "subject", type: "string", description: "Subject entity (optional)" },
|
||||||
{ name: "predicate", type: "string", description: "Predicate/relationship (optional)" },
|
{ name: "predicate", type: "string", description: "Predicate/relationship (optional)" },
|
||||||
|
|
@ -203,7 +214,7 @@ export class AgentService extends FlowProcessor {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tool) {
|
if (tool !== null) {
|
||||||
tools.push(tool);
|
tools.push(tool);
|
||||||
console.log(`[AgentService] Registered tool: ${name} (${implType})`);
|
console.log(`[AgentService] Registered tool: ${name} (${implType})`);
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +287,7 @@ export class AgentService extends FlowProcessor {
|
||||||
flowCtx: FlowContext,
|
flowCtx: FlowContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const requestId = properties.id;
|
const requestId = properties.id;
|
||||||
if (!requestId) return;
|
if (requestId === undefined || requestId.length === 0) return;
|
||||||
|
|
||||||
const responseProducer = flowCtx.flow.producer<AgentResponse>("agent-response");
|
const responseProducer = flowCtx.flow.producer<AgentResponse>("agent-response");
|
||||||
|
|
||||||
|
|
@ -290,7 +301,7 @@ export class AgentService extends FlowProcessor {
|
||||||
// Build tools — config-driven or hardcoded fallback
|
// Build tools — config-driven or hardcoded fallback
|
||||||
let tools: AgentTool[];
|
let tools: AgentTool[];
|
||||||
|
|
||||||
if (this.configuredTools) {
|
if (this.configuredTools !== null) {
|
||||||
tools = this.wireTools(this.configuredTools, flowCtx, msg.collection, onExplain);
|
tools = this.wireTools(this.configuredTools, flowCtx, msg.collection, onExplain);
|
||||||
} else {
|
} else {
|
||||||
// Hardcoded fallback (backward compat)
|
// Hardcoded fallback (backward compat)
|
||||||
|
|
@ -339,7 +350,7 @@ export class AgentService extends FlowProcessor {
|
||||||
prompt: conversation,
|
prompt: conversation,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (llmResponse.error) {
|
if (llmResponse.error !== undefined) {
|
||||||
await responseProducer.send(requestId, {
|
await responseProducer.send(requestId, {
|
||||||
chunk_type: "error",
|
chunk_type: "error",
|
||||||
content: `LLM error: ${llmResponse.error.message}`,
|
content: `LLM error: ${llmResponse.error.message}`,
|
||||||
|
|
@ -354,7 +365,7 @@ export class AgentService extends FlowProcessor {
|
||||||
const parsed = parseReActResponse(text);
|
const parsed = parseReActResponse(text);
|
||||||
|
|
||||||
// Send thought chunk
|
// Send thought chunk
|
||||||
if (parsed.thought) {
|
if (parsed.thought.length > 0) {
|
||||||
await responseProducer.send(requestId, {
|
await responseProducer.send(requestId, {
|
||||||
chunk_type: "thought",
|
chunk_type: "thought",
|
||||||
content: parsed.thought,
|
content: parsed.thought,
|
||||||
|
|
@ -363,7 +374,7 @@ export class AgentService extends FlowProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we got a final answer, emit explain events then send the answer
|
// If we got a final answer, emit explain events then send the answer
|
||||||
if (parsed.finalAnswer) {
|
if (parsed.finalAnswer.length > 0) {
|
||||||
// Emit explain events collected from tool calls
|
// Emit explain events collected from tool calls
|
||||||
for (const explain of explainEvents) {
|
for (const explain of explainEvents) {
|
||||||
await responseProducer.send(requestId, {
|
await responseProducer.send(requestId, {
|
||||||
|
|
@ -384,11 +395,11 @@ export class AgentService extends FlowProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute tool if action was specified
|
// Execute tool if action was specified
|
||||||
if (parsed.action && parsed.actionInput) {
|
if (parsed.action.length > 0 && parsed.actionInput.length > 0) {
|
||||||
const tool = tools.find((t) => t.name === parsed.action);
|
const tool = tools.find((t) => t.name === parsed.action);
|
||||||
let observation: string;
|
let observation: string;
|
||||||
|
|
||||||
if (tool) {
|
if (tool !== undefined) {
|
||||||
try {
|
try {
|
||||||
observation = await tool.execute(parsed.actionInput);
|
observation = await tool.execute(parsed.actionInput);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -407,7 +418,7 @@ export class AgentService extends FlowProcessor {
|
||||||
|
|
||||||
// Append the full exchange to conversation for the next iteration
|
// Append the full exchange to conversation for the next iteration
|
||||||
conversation += `\n${text}\nObservation: ${observation}\n`;
|
conversation += `\n${text}\nObservation: ${observation}\n`;
|
||||||
} else if (!parsed.finalAnswer) {
|
} else if (parsed.finalAnswer.length === 0) {
|
||||||
// LLM didn't produce a valid action or final answer -- nudge it
|
// LLM didn't produce a valid action or final answer -- nudge it
|
||||||
conversation += `\n${text}\nObservation: You must either use a tool (Action + Action Input) or provide a Final Answer.\n`;
|
conversation += `\n${text}\nObservation: You must either use a tool (Action + Action Input) or provide a Final Answer.\n`;
|
||||||
}
|
}
|
||||||
|
|
@ -464,30 +475,31 @@ function parseReActResponse(text: string): {
|
||||||
// Everything from "Final Answer:" to end of text is the answer
|
// Everything from "Final Answer:" to end of text is the answer
|
||||||
const firstLine = trimmed.slice("Final Answer:".length).trim();
|
const firstLine = trimmed.slice("Final Answer:".length).trim();
|
||||||
const remainingLines = lines.slice(i + 1).join("\n").trim();
|
const remainingLines = lines.slice(i + 1).join("\n").trim();
|
||||||
finalAnswer = firstLine + (remainingLines ? "\n" + remainingLines : "");
|
finalAnswer =
|
||||||
|
firstLine + (remainingLines.length > 0 ? "\n" + remainingLines : "");
|
||||||
break;
|
break;
|
||||||
} else if (trimmed.startsWith("Thought:")) {
|
} else if (trimmed.startsWith("Thought:")) {
|
||||||
currentSection = "thought";
|
currentSection = "thought";
|
||||||
const content = trimmed.slice("Thought:".length).trim();
|
const content = trimmed.slice("Thought:".length).trim();
|
||||||
if (content) {
|
if (content.length > 0) {
|
||||||
thought += (thought ? "\n" : "") + content;
|
thought += (thought.length > 0 ? "\n" : "") + content;
|
||||||
}
|
}
|
||||||
} else if (trimmed.startsWith("Action Input:")) {
|
} else if (trimmed.startsWith("Action Input:")) {
|
||||||
currentSection = "action_input";
|
currentSection = "action_input";
|
||||||
const content = trimmed.slice("Action Input:".length).trim();
|
const content = trimmed.slice("Action Input:".length).trim();
|
||||||
if (content) {
|
if (content.length > 0) {
|
||||||
actionInput += content;
|
actionInput += content;
|
||||||
}
|
}
|
||||||
} else if (trimmed.startsWith("Action:")) {
|
} else if (trimmed.startsWith("Action:")) {
|
||||||
currentSection = "action";
|
currentSection = "action";
|
||||||
const content = trimmed.slice("Action:".length).trim();
|
const content = trimmed.slice("Action:".length).trim();
|
||||||
if (content) {
|
if (content.length > 0) {
|
||||||
action = content;
|
action = content;
|
||||||
}
|
}
|
||||||
} else if (trimmed.startsWith("Observation:")) {
|
} else if (trimmed.startsWith("Observation:")) {
|
||||||
// Stop processing -- observations are injected by us, not the LLM
|
// Stop processing -- observations are injected by us, not the LLM
|
||||||
currentSection = null;
|
currentSection = null;
|
||||||
} else if (trimmed.length > 0 && currentSection) {
|
} else if (trimmed.length > 0 && currentSection !== null) {
|
||||||
// Continuation line for current section
|
// Continuation line for current section
|
||||||
switch (currentSection) {
|
switch (currentSection) {
|
||||||
case "thought":
|
case "thought":
|
||||||
|
|
@ -512,6 +524,11 @@ function parseReActResponse(text: string): {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const program = makeProcessorProgram({
|
||||||
|
id: "agent",
|
||||||
|
make: (config) => new AgentService(config),
|
||||||
|
});
|
||||||
|
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
await AgentService.launch("agent");
|
await AgentService.launch("agent");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
RequestResponse,
|
FlowRequestor,
|
||||||
GraphRagRequest,
|
GraphRagRequest,
|
||||||
GraphRagResponse,
|
GraphRagResponse,
|
||||||
DocumentRagRequest,
|
DocumentRagRequest,
|
||||||
|
|
@ -68,7 +68,7 @@ export interface ExplainData {
|
||||||
* Query the knowledge graph for information about entities and their relationships.
|
* Query the knowledge graph for information about entities and their relationships.
|
||||||
*/
|
*/
|
||||||
export function createKnowledgeQueryTool(
|
export function createKnowledgeQueryTool(
|
||||||
client: RequestResponse<GraphRagRequest, GraphRagResponse>,
|
client: FlowRequestor<GraphRagRequest, GraphRagResponse>,
|
||||||
collection?: string,
|
collection?: string,
|
||||||
onExplain?: (data: ExplainData) => void,
|
onExplain?: (data: ExplainData) => void,
|
||||||
): AgentTool {
|
): AgentTool {
|
||||||
|
|
@ -86,19 +86,27 @@ export function createKnowledgeQueryTool(
|
||||||
async execute(input: string): Promise<string> {
|
async execute(input: string): Promise<string> {
|
||||||
const question = parseQuestion(input);
|
const question = parseQuestion(input);
|
||||||
console.log(`[KnowledgeQuery] Executing: "${question.slice(0, 60)}..." collection=${collection}`);
|
console.log(`[KnowledgeQuery] Executing: "${question.slice(0, 60)}..." collection=${collection}`);
|
||||||
const res = await client.request({ query: question, collection });
|
const request: GraphRagRequest = {
|
||||||
console.log(`[KnowledgeQuery] Response (${res.response?.length ?? 0} chars): ${res.error ? `ERROR: ${res.error.message}` : `${res.response?.slice(0, 300)}...`}`);
|
query: question,
|
||||||
|
...(collection !== undefined ? { collection } : {}),
|
||||||
|
};
|
||||||
|
const res = await client.request(request);
|
||||||
|
console.log(`[KnowledgeQuery] Response (${res.response?.length ?? 0} chars): ${res.error !== undefined ? `ERROR: ${res.error.message}` : `${res.response?.slice(0, 300)}...`}`);
|
||||||
|
|
||||||
// Extract explain data if embedded in the response
|
// Extract explain data if embedded in the response
|
||||||
const rawRes = res as Record<string, unknown>;
|
const rawRes = res as Record<string, unknown>;
|
||||||
if (rawRes.message_type === "explain" && rawRes.explain_triples && onExplain) {
|
if (
|
||||||
|
rawRes.message_type === "explain" &&
|
||||||
|
rawRes.explain_triples !== undefined &&
|
||||||
|
onExplain !== undefined
|
||||||
|
) {
|
||||||
onExplain({
|
onExplain({
|
||||||
explainId: (rawRes.explain_id as string) ?? "",
|
explainId: (rawRes.explain_id as string) ?? "",
|
||||||
triples: rawRes.explain_triples as Triple[],
|
triples: rawRes.explain_triples as Triple[],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.error) return `Error: ${res.error.message}`;
|
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||||
return res.response;
|
return res.response;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -108,7 +116,7 @@ export function createKnowledgeQueryTool(
|
||||||
* Search documents for relevant information.
|
* Search documents for relevant information.
|
||||||
*/
|
*/
|
||||||
export function createDocumentQueryTool(
|
export function createDocumentQueryTool(
|
||||||
client: RequestResponse<DocumentRagRequest, DocumentRagResponse>,
|
client: FlowRequestor<DocumentRagRequest, DocumentRagResponse>,
|
||||||
collection?: string,
|
collection?: string,
|
||||||
): AgentTool {
|
): AgentTool {
|
||||||
return {
|
return {
|
||||||
|
|
@ -124,8 +132,12 @@ export function createDocumentQueryTool(
|
||||||
],
|
],
|
||||||
async execute(input: string): Promise<string> {
|
async execute(input: string): Promise<string> {
|
||||||
const question = parseQuestion(input);
|
const question = parseQuestion(input);
|
||||||
const res = await client.request({ query: question, collection });
|
const request: DocumentRagRequest = {
|
||||||
if (res.error) return `Error: ${res.error.message}`;
|
query: question,
|
||||||
|
...(collection !== undefined ? { collection } : {}),
|
||||||
|
};
|
||||||
|
const res = await client.request(request);
|
||||||
|
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||||
return res.response;
|
return res.response;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -153,13 +165,20 @@ function parseTriplesInput(input: string): {
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
const result: {
|
||||||
s: toTerm(parsed.subject ?? parsed.s),
|
s?: Term;
|
||||||
p: toTerm(parsed.predicate ?? parsed.p),
|
p?: Term;
|
||||||
o: toTerm(parsed.object ?? parsed.o),
|
o?: Term;
|
||||||
limit:
|
limit?: number;
|
||||||
typeof parsed.limit === "number" ? parsed.limit : undefined,
|
} = {};
|
||||||
};
|
const s = toTerm(parsed.subject ?? parsed.s);
|
||||||
|
const p = toTerm(parsed.predicate ?? parsed.p);
|
||||||
|
const o = toTerm(parsed.object ?? parsed.o);
|
||||||
|
if (s !== undefined) result.s = s;
|
||||||
|
if (p !== undefined) result.p = p;
|
||||||
|
if (o !== undefined) result.o = o;
|
||||||
|
if (typeof parsed.limit === "number") result.limit = parsed.limit;
|
||||||
|
return result;
|
||||||
} catch {
|
} catch {
|
||||||
// If not valid JSON, treat as a subject search
|
// If not valid JSON, treat as a subject search
|
||||||
return {
|
return {
|
||||||
|
|
@ -172,7 +191,7 @@ function parseTriplesInput(input: string): {
|
||||||
* Query for specific triples (subject-predicate-object relationships) in the knowledge graph.
|
* Query for specific triples (subject-predicate-object relationships) in the knowledge graph.
|
||||||
*/
|
*/
|
||||||
export function createTriplesQueryTool(
|
export function createTriplesQueryTool(
|
||||||
client: RequestResponse<TriplesQueryRequest, TriplesQueryResponse>,
|
client: FlowRequestor<TriplesQueryRequest, TriplesQueryResponse>,
|
||||||
collection?: string,
|
collection?: string,
|
||||||
): AgentTool {
|
): AgentTool {
|
||||||
return {
|
return {
|
||||||
|
|
@ -199,17 +218,18 @@ export function createTriplesQueryTool(
|
||||||
],
|
],
|
||||||
async execute(input: string): Promise<string> {
|
async execute(input: string): Promise<string> {
|
||||||
const { s, p, o, limit } = parseTriplesInput(input);
|
const { s, p, o, limit } = parseTriplesInput(input);
|
||||||
const res = await client.request({
|
const request: TriplesQueryRequest = {
|
||||||
s,
|
|
||||||
p,
|
|
||||||
o,
|
|
||||||
collection,
|
|
||||||
limit: limit ?? 20,
|
limit: limit ?? 20,
|
||||||
});
|
...(s !== undefined ? { s } : {}),
|
||||||
|
...(p !== undefined ? { p } : {}),
|
||||||
|
...(o !== undefined ? { o } : {}),
|
||||||
|
...(collection !== undefined ? { collection } : {}),
|
||||||
|
};
|
||||||
|
const res = await client.request(request);
|
||||||
|
|
||||||
if (res.error) return `Error: ${res.error.message}`;
|
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||||
|
|
||||||
if (!res.triples || res.triples.length === 0) {
|
if (res.triples === undefined || res.triples.length === 0) {
|
||||||
return "No triples found matching the query.";
|
return "No triples found matching the query.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,7 +249,7 @@ export function createTriplesQueryTool(
|
||||||
* this function just wraps it as an AgentTool the ReAct agent can invoke.
|
* this function just wraps it as an AgentTool the ReAct agent can invoke.
|
||||||
*/
|
*/
|
||||||
export function createMcpTool(
|
export function createMcpTool(
|
||||||
client: RequestResponse<ToolRequest, ToolResponse>,
|
client: FlowRequestor<ToolRequest, ToolResponse>,
|
||||||
toolName: string,
|
toolName: string,
|
||||||
description: string,
|
description: string,
|
||||||
args: ToolArg[],
|
args: ToolArg[],
|
||||||
|
|
@ -240,9 +260,9 @@ export function createMcpTool(
|
||||||
args,
|
args,
|
||||||
async execute(input: string): Promise<string> {
|
async execute(input: string): Promise<string> {
|
||||||
const res = await client.request({ name: toolName, parameters: input });
|
const res = await client.request({ name: toolName, parameters: input });
|
||||||
if (res.error) return `Error: ${res.error.message}`;
|
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||||
if (res.text) return res.text;
|
if (res.text !== undefined) return res.text;
|
||||||
if (res.object) return res.object;
|
if (res.object !== undefined) return res.object;
|
||||||
return "No content";
|
return "No content";
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export function filterToolsByGroupAndState(
|
||||||
currentState?: string,
|
currentState?: string,
|
||||||
): AgentTool[] {
|
): AgentTool[] {
|
||||||
const groups = requestedGroups ?? ["default"];
|
const groups = requestedGroups ?? ["default"];
|
||||||
const state = currentState || "undefined";
|
const state = currentState ?? "undefined";
|
||||||
|
|
||||||
return tools.filter((tool) => isToolAvailable(tool, groups, state));
|
return tools.filter((tool) => isToolAvailable(tool, groups, state));
|
||||||
}
|
}
|
||||||
|
|
@ -31,12 +31,12 @@ function isToolAvailable(
|
||||||
|
|
||||||
// Get tool groups (default to ["default"])
|
// Get tool groups (default to ["default"])
|
||||||
let toolGroups = config["group"] as string[] | string | undefined;
|
let toolGroups = config["group"] as string[] | string | undefined;
|
||||||
if (!toolGroups) toolGroups = ["default"];
|
if (toolGroups === undefined) toolGroups = ["default"];
|
||||||
if (!Array.isArray(toolGroups)) toolGroups = [toolGroups];
|
if (!Array.isArray(toolGroups)) toolGroups = [toolGroups];
|
||||||
|
|
||||||
// Get tool applicable states (default to ["*"] = all states)
|
// Get tool applicable states (default to ["*"] = all states)
|
||||||
let applicableStates = config["applicable-states"] as string[] | string | undefined;
|
let applicableStates = config["applicable-states"] as string[] | string | undefined;
|
||||||
if (!applicableStates) applicableStates = ["*"];
|
if (applicableStates === undefined) applicableStates = ["*"];
|
||||||
if (!Array.isArray(applicableStates)) applicableStates = [applicableStates];
|
if (!Array.isArray(applicableStates)) applicableStates = [applicableStates];
|
||||||
|
|
||||||
// Group match: wildcard in requested groups, or intersection non-empty
|
// Group match: wildcard in requested groups, or intersection non-empty
|
||||||
|
|
@ -57,5 +57,5 @@ function isToolAvailable(
|
||||||
*/
|
*/
|
||||||
export function getNextState(tool: AgentTool, currentState: string): string {
|
export function getNextState(tool: AgentTool, currentState: string): string {
|
||||||
const nextState = tool.config?.["state"] as string | undefined;
|
const nextState = tool.config?.["state"] as string | undefined;
|
||||||
return nextState || currentState;
|
return nextState ?? currentState;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,14 @@ import {
|
||||||
ParameterSpec,
|
ParameterSpec,
|
||||||
type ProcessorConfig,
|
type ProcessorConfig,
|
||||||
type FlowContext,
|
type FlowContext,
|
||||||
|
type FlowResourceNotFoundError,
|
||||||
|
type MessagingDeliveryError,
|
||||||
type TextDocument,
|
type TextDocument,
|
||||||
type Chunk,
|
type Chunk,
|
||||||
type Triples,
|
type Triples,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
|
import { makeProcessorProgram } from "@trustgraph/base";
|
||||||
|
import { Effect } from "effect";
|
||||||
import { recursiveSplit } from "./recursive-splitter.js";
|
import { recursiveSplit } from "./recursive-splitter.js";
|
||||||
|
|
||||||
const DEFAULT_CHUNK_SIZE = 2000;
|
const DEFAULT_CHUNK_SIZE = 2000;
|
||||||
|
|
@ -30,7 +34,10 @@ export class ChunkingService extends FlowProcessor {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
this.registerSpecification(
|
this.registerSpecification(
|
||||||
new ConsumerSpec<TextDocument>("chunk-input", this.onMessage.bind(this)),
|
new ConsumerSpec<TextDocument, FlowResourceNotFoundError | MessagingDeliveryError>(
|
||||||
|
"chunk-input",
|
||||||
|
this.onMessageEffect.bind(this),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
this.registerSpecification(new ProducerSpec<Chunk>("chunk-output"));
|
this.registerSpecification(new ProducerSpec<Chunk>("chunk-output"));
|
||||||
this.registerSpecification(new ProducerSpec<Triples>("chunk-triples"));
|
this.registerSpecification(new ProducerSpec<Triples>("chunk-triples"));
|
||||||
|
|
@ -40,55 +47,55 @@ export class ChunkingService extends FlowProcessor {
|
||||||
console.log("[ChunkingService] Service initialized");
|
console.log("[ChunkingService] Service initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onMessage(
|
private onMessageEffect(
|
||||||
msg: TextDocument,
|
msg: TextDocument,
|
||||||
properties: Record<string, string>,
|
properties: Record<string, string>,
|
||||||
flowCtx: FlowContext,
|
flowCtx: FlowContext,
|
||||||
): Promise<void> {
|
) {
|
||||||
const requestId = properties.id;
|
return Effect.gen(function* () {
|
||||||
if (!requestId) return;
|
const requestId = properties.id;
|
||||||
|
if (requestId === undefined || requestId.length === 0) return;
|
||||||
|
|
||||||
let chunkSize: number;
|
const chunkSize = yield* flowCtx.flow.parameterEffect<number>("chunk-size").pipe(
|
||||||
let chunkOverlap: number;
|
Effect.catch(() => Effect.succeed(DEFAULT_CHUNK_SIZE)),
|
||||||
|
);
|
||||||
|
const chunkOverlap = yield* flowCtx.flow.parameterEffect<number>("chunk-overlap").pipe(
|
||||||
|
Effect.catch(() => Effect.succeed(DEFAULT_CHUNK_OVERLAP)),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
const text = msg.text;
|
||||||
chunkSize = flowCtx.flow.parameter<number>("chunk-size");
|
if (text.trim().length === 0) {
|
||||||
} catch {
|
yield* Effect.logWarning(`[ChunkingService] Empty text received for document ${msg.documentId}`);
|
||||||
chunkSize = DEFAULT_CHUNK_SIZE;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const chunks = recursiveSplit(text, chunkSize, chunkOverlap);
|
||||||
chunkOverlap = flowCtx.flow.parameter<number>("chunk-overlap");
|
|
||||||
} catch {
|
|
||||||
chunkOverlap = DEFAULT_CHUNK_OVERLAP;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = msg.text;
|
yield* Effect.log(
|
||||||
if (!text || text.trim().length === 0) {
|
`[ChunkingService] Split document ${msg.documentId} into ${chunks.length} chunks (size=${chunkSize}, overlap=${chunkOverlap})`,
|
||||||
console.warn(`[ChunkingService] Empty text received for document ${msg.documentId}`);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunks = recursiveSplit(text, chunkSize, chunkOverlap);
|
const outputProducer = yield* flowCtx.flow.producerEffect<Chunk>("chunk-output");
|
||||||
|
|
||||||
console.log(
|
yield* Effect.forEach(
|
||||||
`[ChunkingService] Split document ${msg.documentId} into ${chunks.length} chunks (size=${chunkSize}, overlap=${chunkOverlap})`,
|
chunks,
|
||||||
);
|
(chunkText) =>
|
||||||
|
outputProducer.send(requestId, {
|
||||||
const outputProducer = flowCtx.flow.producer<Chunk>("chunk-output");
|
metadata: msg.metadata,
|
||||||
|
chunk: chunkText,
|
||||||
for (const chunkText of chunks) {
|
documentId: msg.documentId,
|
||||||
const chunk: Chunk = {
|
}),
|
||||||
metadata: msg.metadata,
|
{ discard: true },
|
||||||
chunk: chunkText,
|
);
|
||||||
documentId: msg.documentId,
|
});
|
||||||
};
|
|
||||||
|
|
||||||
await outputProducer.send(requestId, chunk);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const program = makeProcessorProgram({
|
||||||
|
id: "chunking",
|
||||||
|
make: (config) => new ChunkingService(config),
|
||||||
|
});
|
||||||
|
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
await ChunkingService.launch("chunking");
|
await ChunkingService.launch("chunking");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,24 @@
|
||||||
* Python reference: trustgraph-flow/trustgraph/config/service/service.py
|
* Python reference: trustgraph-flow/trustgraph/config/service/service.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
import { Effect } from "effect";
|
||||||
import { dirname } from "node:path";
|
import * as S from "effect/Schema";
|
||||||
import {
|
import {
|
||||||
AsyncProcessor,
|
AsyncProcessor,
|
||||||
type ProcessorConfig,
|
type ProcessorConfig,
|
||||||
topics,
|
topics,
|
||||||
|
ConfigRequest as ConfigRequestSchema,
|
||||||
|
ConfigResponse as ConfigResponseSchema,
|
||||||
type ConfigRequest,
|
type ConfigRequest,
|
||||||
type ConfigResponse,
|
type ConfigResponse,
|
||||||
type ConfigOperation,
|
type ConfigOperation,
|
||||||
|
errorMessage,
|
||||||
|
loadProcessorRuntimeConfig,
|
||||||
|
makeProcessorProgram,
|
||||||
|
optionalStringConfig,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
import type { PubSubBackend, BackendProducer, BackendConsumer, Message } from "@trustgraph/base";
|
import type { BackendProducer, BackendConsumer, Message } from "@trustgraph/base";
|
||||||
|
import { readTextFile, writeTextFile } from "../runtime/effect-files.js";
|
||||||
|
|
||||||
export interface ConfigServiceConfig extends ProcessorConfig {
|
export interface ConfigServiceConfig extends ProcessorConfig {
|
||||||
persistPath?: string;
|
persistPath?: string;
|
||||||
|
|
@ -32,6 +39,11 @@ interface ConfigPush {
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ConfigPushSchema = S.Struct({
|
||||||
|
version: S.Number,
|
||||||
|
config: S.Record(S.String, S.Unknown),
|
||||||
|
});
|
||||||
|
|
||||||
export class ConfigService extends AsyncProcessor {
|
export class ConfigService extends AsyncProcessor {
|
||||||
private store = new Map<string, Map<string, unknown>>();
|
private store = new Map<string, Map<string, unknown>>();
|
||||||
private version = 0;
|
private version = 0;
|
||||||
|
|
@ -42,27 +54,30 @@ export class ConfigService extends AsyncProcessor {
|
||||||
|
|
||||||
constructor(config: ConfigServiceConfig) {
|
constructor(config: ConfigServiceConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
this.persistPath = config.persistPath ?? process.env.CONFIG_PERSIST_PATH ?? null;
|
this.persistPath = config.persistPath ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async run(): Promise<void> {
|
protected override async run(): Promise<void> {
|
||||||
// Optionally load persisted state
|
// Optionally load persisted state
|
||||||
if (this.persistPath) {
|
if (this.persistPath !== null) {
|
||||||
await this.loadFromDisk();
|
await this.loadFromDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create producers
|
// Create producers
|
||||||
this.responseProducer = await this.pubsub.createProducer<ConfigResponse>({
|
this.responseProducer = await this.pubsub.createProducer<ConfigResponse>({
|
||||||
topic: topics.configResponse,
|
topic: topics.configResponse,
|
||||||
|
schema: ConfigResponseSchema,
|
||||||
});
|
});
|
||||||
this.pushProducer = await this.pubsub.createProducer<ConfigPush>({
|
this.pushProducer = await this.pubsub.createProducer<ConfigPush>({
|
||||||
topic: topics.configPush,
|
topic: topics.configPush,
|
||||||
|
schema: ConfigPushSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create consumer for config requests
|
// Create consumer for config requests
|
||||||
this.consumer = await this.pubsub.createConsumer<ConfigRequest>({
|
this.consumer = await this.pubsub.createConsumer<ConfigRequest>({
|
||||||
topic: topics.configRequest,
|
topic: topics.configRequest,
|
||||||
subscription: `${this.config.id}-config-request`,
|
subscription: `${this.config.id}-config-request`,
|
||||||
|
schema: ConfigRequestSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Push initial config
|
// Push initial config
|
||||||
|
|
@ -73,11 +88,14 @@ export class ConfigService extends AsyncProcessor {
|
||||||
// Main consume loop
|
// Main consume loop
|
||||||
while (this.running) {
|
while (this.running) {
|
||||||
try {
|
try {
|
||||||
const msg = await this.consumer.receive(2000);
|
const consumer = this.consumer;
|
||||||
if (!msg) continue;
|
if (consumer === null) throw new Error("Config consumer not started");
|
||||||
|
|
||||||
|
const msg = await consumer.receive(2000);
|
||||||
|
if (msg === null) continue;
|
||||||
|
|
||||||
await this.handleMessage(msg);
|
await this.handleMessage(msg);
|
||||||
await this.consumer.acknowledge(msg);
|
await consumer.acknowledge(msg);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!this.running) break;
|
if (!this.running) break;
|
||||||
console.error("[ConfigService] Error in consume loop:", err);
|
console.error("[ConfigService] Error in consume loop:", err);
|
||||||
|
|
@ -87,21 +105,25 @@ export class ConfigService extends AsyncProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleMessage(msg: Message<ConfigRequest>): Promise<void> {
|
private async handleMessage(msg: Message<ConfigRequest>): Promise<void> {
|
||||||
const request = msg.value();
|
const request = await Effect.runPromise(S.decodeUnknownEffect(ConfigRequestSchema)(msg.value()));
|
||||||
const props = msg.properties();
|
const props = msg.properties();
|
||||||
const requestId = props.id;
|
const requestId = props.id;
|
||||||
|
|
||||||
if (!requestId) {
|
if (requestId === undefined || requestId.length === 0) {
|
||||||
console.warn("[ConfigService] Received request without id, ignoring");
|
console.warn("[ConfigService] Received request without id, ignoring");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.handleOperation(request);
|
const response = await this.handleOperation(request);
|
||||||
await this.responseProducer!.send(response, { id: requestId });
|
const responseProducer = this.responseProducer;
|
||||||
|
if (responseProducer === null) throw new Error("Config response producer not started");
|
||||||
|
await responseProducer.send(response, { id: requestId });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = errorMessage(err);
|
||||||
await this.responseProducer!.send(
|
const responseProducer = this.responseProducer;
|
||||||
|
if (responseProducer === null) throw new Error("Config response producer not started");
|
||||||
|
await responseProducer.send(
|
||||||
{
|
{
|
||||||
error: { type: "config-error", message },
|
error: { type: "config-error", message },
|
||||||
},
|
},
|
||||||
|
|
@ -146,7 +168,7 @@ export class ConfigService extends AsyncProcessor {
|
||||||
const namespace = keys[0];
|
const namespace = keys[0];
|
||||||
const subMap = this.store.get(namespace);
|
const subMap = this.store.get(namespace);
|
||||||
|
|
||||||
if (subMap) {
|
if (subMap !== undefined) {
|
||||||
if (keys.length === 1) {
|
if (keys.length === 1) {
|
||||||
// Return entire namespace
|
// Return entire namespace
|
||||||
for (const [k, v] of subMap) {
|
for (const [k, v] of subMap) {
|
||||||
|
|
@ -176,7 +198,7 @@ export class ConfigService extends AsyncProcessor {
|
||||||
|
|
||||||
const namespace = keys[0];
|
const namespace = keys[0];
|
||||||
let subMap = this.store.get(namespace);
|
let subMap = this.store.get(namespace);
|
||||||
if (!subMap) {
|
if (subMap === undefined) {
|
||||||
subMap = new Map<string, unknown>();
|
subMap = new Map<string, unknown>();
|
||||||
this.store.set(namespace, subMap);
|
this.store.set(namespace, subMap);
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +227,7 @@ export class ConfigService extends AsyncProcessor {
|
||||||
} else {
|
} else {
|
||||||
// Delete specific keys within namespace
|
// Delete specific keys within namespace
|
||||||
const subMap = this.store.get(namespace);
|
const subMap = this.store.get(namespace);
|
||||||
if (subMap) {
|
if (subMap !== undefined) {
|
||||||
for (let i = 1; i < keys.length; i++) {
|
for (let i = 1; i < keys.length; i++) {
|
||||||
subMap.delete(keys[i]);
|
subMap.delete(keys[i]);
|
||||||
}
|
}
|
||||||
|
|
@ -236,7 +258,7 @@ export class ConfigService extends AsyncProcessor {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: this.version,
|
version: this.version,
|
||||||
directory: subMap ? [...subMap.keys()] : [],
|
directory: subMap !== undefined ? [...subMap.keys()] : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,7 +268,12 @@ export class ConfigService extends AsyncProcessor {
|
||||||
const values: { key: string; value: unknown }[] = [];
|
const values: { key: string; value: unknown }[] = [];
|
||||||
|
|
||||||
for (const [namespace, subMap] of this.store) {
|
for (const [namespace, subMap] of this.store) {
|
||||||
if (!type || namespace === type || namespace.startsWith(`${type}.`) || namespace.startsWith(`${type}/`)) {
|
if (
|
||||||
|
type.length === 0 ||
|
||||||
|
namespace === type ||
|
||||||
|
namespace.startsWith(`${type}.`) ||
|
||||||
|
namespace.startsWith(`${type}/`)
|
||||||
|
) {
|
||||||
for (const [k, v] of subMap) {
|
for (const [k, v] of subMap) {
|
||||||
values.push({ key: `${namespace}.${k}`, value: v });
|
values.push({ key: `${namespace}.${k}`, value: v });
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +301,8 @@ export class ConfigService extends AsyncProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pushConfig(): Promise<void> {
|
private async pushConfig(): Promise<void> {
|
||||||
if (!this.pushProducer) return;
|
const pushProducer = this.pushProducer;
|
||||||
|
if (pushProducer === null) return;
|
||||||
|
|
||||||
const config: Record<string, unknown> = {};
|
const config: Record<string, unknown> = {};
|
||||||
for (const [namespace, subMap] of this.store) {
|
for (const [namespace, subMap] of this.store) {
|
||||||
|
|
@ -285,7 +313,7 @@ export class ConfigService extends AsyncProcessor {
|
||||||
config[namespace] = obj;
|
config[namespace] = obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.pushProducer.send({
|
await pushProducer.send({
|
||||||
version: this.version,
|
version: this.version,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
|
@ -294,7 +322,8 @@ export class ConfigService extends AsyncProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async persist(): Promise<void> {
|
private async persist(): Promise<void> {
|
||||||
if (!this.persistPath) return;
|
const persistPath = this.persistPath;
|
||||||
|
if (persistPath === null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data: Record<string, Record<string, unknown>> = {};
|
const data: Record<string, Record<string, unknown>> = {};
|
||||||
|
|
@ -313,18 +342,18 @@ export class ConfigService extends AsyncProcessor {
|
||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
|
|
||||||
await mkdir(dirname(this.persistPath), { recursive: true });
|
await writeTextFile(persistPath, json);
|
||||||
await writeFile(this.persistPath, json, "utf-8");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[ConfigService] Failed to persist config:", err);
|
await Effect.runPromise(Effect.logError("[ConfigService] Failed to persist config", { error: errorMessage(err) }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadFromDisk(): Promise<void> {
|
private async loadFromDisk(): Promise<void> {
|
||||||
if (!this.persistPath) return;
|
const persistPath = this.persistPath;
|
||||||
|
if (persistPath === null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = await readFile(this.persistPath, "utf-8");
|
const raw = await readTextFile(persistPath);
|
||||||
const parsed = JSON.parse(raw) as {
|
const parsed = JSON.parse(raw) as {
|
||||||
version: number;
|
version: number;
|
||||||
data: Record<string, Record<string, unknown>>;
|
data: Record<string, Record<string, unknown>>;
|
||||||
|
|
@ -346,20 +375,20 @@ export class ConfigService extends AsyncProcessor {
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// File doesn't exist yet or is invalid — start fresh
|
// File doesn't exist yet or is invalid — start fresh
|
||||||
console.log("[ConfigService] No persisted config found, starting fresh");
|
await Effect.runPromise(Effect.log("[ConfigService] No persisted config found, starting fresh"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override async stop(): Promise<void> {
|
override async stop(): Promise<void> {
|
||||||
if (this.consumer) {
|
if (this.consumer !== null) {
|
||||||
await this.consumer.close();
|
await this.consumer.close();
|
||||||
this.consumer = null;
|
this.consumer = null;
|
||||||
}
|
}
|
||||||
if (this.responseProducer) {
|
if (this.responseProducer !== null) {
|
||||||
await this.responseProducer.close();
|
await this.responseProducer.close();
|
||||||
this.responseProducer = null;
|
this.responseProducer = null;
|
||||||
}
|
}
|
||||||
if (this.pushProducer) {
|
if (this.pushProducer !== null) {
|
||||||
await this.pushProducer.close();
|
await this.pushProducer.close();
|
||||||
this.pushProducer = null;
|
this.pushProducer = null;
|
||||||
}
|
}
|
||||||
|
|
@ -371,6 +400,23 @@ function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadConfigServiceRuntimeConfig = Effect.fn("loadConfigServiceRuntimeConfig")(function* () {
|
||||||
|
const processorConfig = yield* loadProcessorRuntimeConfig("config-svc", {
|
||||||
|
manageProcessSignals: false,
|
||||||
|
});
|
||||||
|
const persistPath = yield* optionalStringConfig("CONFIG_PERSIST_PATH");
|
||||||
|
return {
|
||||||
|
...processorConfig,
|
||||||
|
...(persistPath !== undefined ? { persistPath } : {}),
|
||||||
|
} satisfies ConfigServiceConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const program = makeProcessorProgram({
|
||||||
|
id: "config-svc",
|
||||||
|
loadConfig: loadConfigServiceRuntimeConfig(),
|
||||||
|
make: (config) => new ConfigService(config),
|
||||||
|
});
|
||||||
|
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
await ConfigService.launch("config-svc");
|
await Effect.runPromise(program);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@
|
||||||
* Python reference: trustgraph-flow/trustgraph/knowledge/service/service.py
|
* Python reference: trustgraph-flow/trustgraph/knowledge/service/service.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
||||||
import { dirname, join } from "node:path";
|
|
||||||
import {
|
import {
|
||||||
AsyncProcessor,
|
AsyncProcessor,
|
||||||
type ProcessorConfig,
|
type ProcessorConfig,
|
||||||
|
|
@ -21,7 +19,9 @@ import {
|
||||||
type Triple,
|
type Triple,
|
||||||
type Term,
|
type Term,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
|
import { makeProcessorProgram } from "@trustgraph/base";
|
||||||
import type { BackendProducer, BackendConsumer, Message } from "@trustgraph/base";
|
import type { BackendProducer, BackendConsumer, Message } from "@trustgraph/base";
|
||||||
|
import { joinPath, readTextFile, writeTextFile } from "../runtime/effect-files.js";
|
||||||
|
|
||||||
export interface KnowledgeCoreServiceConfig extends ProcessorConfig {
|
export interface KnowledgeCoreServiceConfig extends ProcessorConfig {
|
||||||
dataDir?: string;
|
dataDir?: string;
|
||||||
|
|
@ -43,7 +43,7 @@ export class KnowledgeCoreService extends AsyncProcessor {
|
||||||
constructor(config: KnowledgeCoreServiceConfig) {
|
constructor(config: KnowledgeCoreServiceConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
const dataDir = config.dataDir ?? process.env.KNOWLEDGE_DATA_DIR ?? "./data/knowledge";
|
const dataDir = config.dataDir ?? process.env.KNOWLEDGE_DATA_DIR ?? "./data/knowledge";
|
||||||
this.persistPath = join(dataDir, "knowledge-state.json");
|
this.persistPath = joinPath(dataDir, "knowledge-state.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
private coreKey(user: string, id: string): string {
|
private coreKey(user: string, id: string): string {
|
||||||
|
|
@ -71,7 +71,7 @@ export class KnowledgeCoreService extends AsyncProcessor {
|
||||||
while (this.running) {
|
while (this.running) {
|
||||||
try {
|
try {
|
||||||
const msg = await this.consumer.receive(2000);
|
const msg = await this.consumer.receive(2000);
|
||||||
if (!msg) continue;
|
if (msg === null) continue;
|
||||||
|
|
||||||
await this.handleMessage(msg);
|
await this.handleMessage(msg);
|
||||||
await this.consumer.acknowledge(msg);
|
await this.consumer.acknowledge(msg);
|
||||||
|
|
@ -88,7 +88,7 @@ export class KnowledgeCoreService extends AsyncProcessor {
|
||||||
const props = msg.properties();
|
const props = msg.properties();
|
||||||
const requestId = props.id;
|
const requestId = props.id;
|
||||||
|
|
||||||
if (!requestId) {
|
if (requestId === undefined || requestId.length === 0) {
|
||||||
console.warn("[KnowledgeCoreService] Received request without id, ignoring");
|
console.warn("[KnowledgeCoreService] Received request without id, ignoring");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -123,11 +123,11 @@ export class KnowledgeCoreService extends AsyncProcessor {
|
||||||
|
|
||||||
private async listKgCores(request: KnowledgeRequest, requestId: string): Promise<void> {
|
private async listKgCores(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||||
const user = request.user ?? "";
|
const user = request.user ?? "";
|
||||||
const prefix = user ? `${user}:` : "";
|
const prefix = user.length > 0 ? `${user}:` : "";
|
||||||
|
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
for (const key of this.cores.keys()) {
|
for (const key of this.cores.keys()) {
|
||||||
if (!prefix || key.startsWith(prefix)) {
|
if (prefix.length === 0 || key.startsWith(prefix)) {
|
||||||
// Extract the ID portion after the user prefix
|
// Extract the ID portion after the user prefix
|
||||||
const id = key.slice(prefix.length);
|
const id = key.slice(prefix.length);
|
||||||
ids.push(id);
|
ids.push(id);
|
||||||
|
|
@ -143,7 +143,7 @@ export class KnowledgeCoreService extends AsyncProcessor {
|
||||||
const key = this.coreKey(user, coreId);
|
const key = this.coreKey(user, coreId);
|
||||||
|
|
||||||
const core = this.cores.get(key);
|
const core = this.cores.get(key);
|
||||||
if (!core) {
|
if (core === undefined) {
|
||||||
throw new Error(`Knowledge core not found: ${key}`);
|
throw new Error(`Knowledge core not found: ${key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,18 +196,18 @@ export class KnowledgeCoreService extends AsyncProcessor {
|
||||||
const key = this.coreKey(user, coreId);
|
const key = this.coreKey(user, coreId);
|
||||||
|
|
||||||
let core = this.cores.get(key);
|
let core = this.cores.get(key);
|
||||||
if (!core) {
|
if (core === undefined) {
|
||||||
core = { triples: [], graphEmbeddings: [] };
|
core = { triples: [], graphEmbeddings: [] };
|
||||||
this.cores.set(key, core);
|
this.cores.set(key, core);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append triples if provided
|
// Append triples if provided
|
||||||
if (request.triples && request.triples.length > 0) {
|
if (request.triples !== undefined && request.triples.length > 0) {
|
||||||
core.triples.push(...request.triples);
|
core.triples.push(...request.triples);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append graph embeddings if provided
|
// Append graph embeddings if provided
|
||||||
if (request.graphEmbeddings && request.graphEmbeddings.length > 0) {
|
if (request.graphEmbeddings !== undefined && request.graphEmbeddings.length > 0) {
|
||||||
core.graphEmbeddings.push(...request.graphEmbeddings);
|
core.graphEmbeddings.push(...request.graphEmbeddings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,7 +225,7 @@ export class KnowledgeCoreService extends AsyncProcessor {
|
||||||
const key = this.coreKey(user, coreId);
|
const key = this.coreKey(user, coreId);
|
||||||
|
|
||||||
const core = this.cores.get(key);
|
const core = this.cores.get(key);
|
||||||
if (!core) {
|
if (core === undefined) {
|
||||||
throw new Error(`Knowledge core not found: ${key}`);
|
throw new Error(`Knowledge core not found: ${key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,8 +248,7 @@ export class KnowledgeCoreService extends AsyncProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = JSON.stringify(data, null, 2);
|
const json = JSON.stringify(data, null, 2);
|
||||||
await mkdir(dirname(this.persistPath), { recursive: true });
|
await writeTextFile(this.persistPath, json);
|
||||||
await writeFile(this.persistPath, json, "utf-8");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[KnowledgeCoreService] Failed to persist state:", err);
|
console.error("[KnowledgeCoreService] Failed to persist state:", err);
|
||||||
}
|
}
|
||||||
|
|
@ -257,7 +256,7 @@ export class KnowledgeCoreService extends AsyncProcessor {
|
||||||
|
|
||||||
private async loadFromDisk(): Promise<void> {
|
private async loadFromDisk(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const raw = await readFile(this.persistPath, "utf-8");
|
const raw = await readTextFile(this.persistPath);
|
||||||
const parsed = JSON.parse(raw) as Record<string, KnowledgeCore>;
|
const parsed = JSON.parse(raw) as Record<string, KnowledgeCore>;
|
||||||
|
|
||||||
this.cores.clear();
|
this.cores.clear();
|
||||||
|
|
@ -272,11 +271,11 @@ export class KnowledgeCoreService extends AsyncProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
override async stop(): Promise<void> {
|
override async stop(): Promise<void> {
|
||||||
if (this.consumer) {
|
if (this.consumer !== null) {
|
||||||
await this.consumer.close();
|
await this.consumer.close();
|
||||||
this.consumer = null;
|
this.consumer = null;
|
||||||
}
|
}
|
||||||
if (this.responseProducer) {
|
if (this.responseProducer !== null) {
|
||||||
await this.responseProducer.close();
|
await this.responseProducer.close();
|
||||||
this.responseProducer = null;
|
this.responseProducer = null;
|
||||||
}
|
}
|
||||||
|
|
@ -288,6 +287,11 @@ function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const program = makeProcessorProgram({
|
||||||
|
id: "knowledge-svc",
|
||||||
|
make: (config) => new KnowledgeCoreService(config),
|
||||||
|
});
|
||||||
|
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
await KnowledgeCoreService.launch("knowledge-svc");
|
await KnowledgeCoreService.launch("knowledge-svc");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,14 @@ import {
|
||||||
type LibrarianRequest,
|
type LibrarianRequest,
|
||||||
type LibrarianResponse,
|
type LibrarianResponse,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
|
import { makeProcessorProgram } from "@trustgraph/base";
|
||||||
|
|
||||||
export class PdfDecoderService extends FlowProcessor {
|
export class PdfDecoderService extends FlowProcessor {
|
||||||
constructor(config: ProcessorConfig) {
|
constructor(config: ProcessorConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
this.registerSpecification(
|
this.registerSpecification(
|
||||||
new ConsumerSpec<Document>("decode-input", this.onMessage.bind(this)),
|
ConsumerSpec.fromPromise<Document>("decode-input", this.onMessage.bind(this)),
|
||||||
);
|
);
|
||||||
this.registerSpecification(new ProducerSpec<TextDocument>("decode-output"));
|
this.registerSpecification(new ProducerSpec<TextDocument>("decode-output"));
|
||||||
this.registerSpecification(new ProducerSpec<Triples>("decode-triples"));
|
this.registerSpecification(new ProducerSpec<Triples>("decode-triples"));
|
||||||
|
|
@ -57,7 +58,7 @@ export class PdfDecoderService extends FlowProcessor {
|
||||||
flowCtx: FlowContext,
|
flowCtx: FlowContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const requestId = properties.id;
|
const requestId = properties.id;
|
||||||
if (!requestId) return;
|
if (requestId === undefined || requestId.length === 0) return;
|
||||||
|
|
||||||
const { documentId } = msg;
|
const { documentId } = msg;
|
||||||
const user = msg.metadata.user;
|
const user = msg.metadata.user;
|
||||||
|
|
@ -73,7 +74,7 @@ export class PdfDecoderService extends FlowProcessor {
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (metadataResp.error) {
|
if (metadataResp.error !== undefined) {
|
||||||
console.error(
|
console.error(
|
||||||
`[PdfDecoder] Failed to get metadata for ${documentId}:`,
|
`[PdfDecoder] Failed to get metadata for ${documentId}:`,
|
||||||
metadataResp.error.message,
|
metadataResp.error.message,
|
||||||
|
|
@ -96,7 +97,11 @@ export class PdfDecoderService extends FlowProcessor {
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (contentResp.error || !contentResp.content) {
|
if (
|
||||||
|
contentResp.error !== undefined ||
|
||||||
|
contentResp.content === undefined ||
|
||||||
|
contentResp.content.length === 0
|
||||||
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
`[PdfDecoder] Failed to get content for ${documentId}:`,
|
`[PdfDecoder] Failed to get content for ${documentId}:`,
|
||||||
contentResp.error?.message ?? "no content",
|
contentResp.error?.message ?? "no content",
|
||||||
|
|
@ -123,7 +128,7 @@ export class PdfDecoderService extends FlowProcessor {
|
||||||
.map((item) => item.str)
|
.map((item) => item.str)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
if (!pageText.trim()) {
|
if (pageText.trim().length === 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`[PdfDecoder] Skipping empty page ${i} of document ${documentId}`,
|
`[PdfDecoder] Skipping empty page ${i} of document ${documentId}`,
|
||||||
);
|
);
|
||||||
|
|
@ -147,7 +152,7 @@ export class PdfDecoderService extends FlowProcessor {
|
||||||
content: Buffer.from(pageText).toString("base64"),
|
content: Buffer.from(pageText).toString("base64"),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (childResp.error) {
|
if (childResp.error !== undefined) {
|
||||||
console.error(
|
console.error(
|
||||||
`[PdfDecoder] Failed to save page ${i} of ${documentId}:`,
|
`[PdfDecoder] Failed to save page ${i} of ${documentId}:`,
|
||||||
childResp.error.message,
|
childResp.error.message,
|
||||||
|
|
@ -198,6 +203,11 @@ function literalTerm(value: string): Term {
|
||||||
return { type: "LITERAL", value };
|
return { type: "LITERAL", value };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const program = makeProcessorProgram({
|
||||||
|
id: "pdf-decoder",
|
||||||
|
make: (config) => new PdfDecoderService(config),
|
||||||
|
});
|
||||||
|
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
await PdfDecoderService.launch("pdf-decoder");
|
await PdfDecoderService.launch("pdf-decoder");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,112 @@
|
||||||
/**
|
/**
|
||||||
* Ollama embeddings service.
|
* Ollama embeddings provider.
|
||||||
*
|
|
||||||
* Simple HTTP POST to a local Ollama instance to generate embeddings.
|
|
||||||
* Extends EmbeddingsService from @trustgraph/base so it plugs into the
|
|
||||||
* flow processor framework (consumer/producer wiring is handled by the base class).
|
|
||||||
*
|
*
|
||||||
* Python reference: trustgraph-flow/trustgraph/embeddings/ollama/processor.py
|
* Python reference: trustgraph-flow/trustgraph/embeddings/ollama/processor.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Effect, Layer } from "effect";
|
||||||
|
import * as S from "effect/Schema";
|
||||||
import {
|
import {
|
||||||
|
Embeddings,
|
||||||
EmbeddingsService,
|
EmbeddingsService,
|
||||||
|
embeddingsError,
|
||||||
|
type EmbeddingsServiceShape,
|
||||||
type ProcessorConfig,
|
type ProcessorConfig,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
|
import { makeProcessorProgram } from "@trustgraph/base";
|
||||||
|
|
||||||
export interface OllamaEmbeddingsConfig extends ProcessorConfig {
|
export interface OllamaEmbeddingsConfig extends ProcessorConfig {
|
||||||
model?: string;
|
model?: string;
|
||||||
ollamaHost?: string;
|
ollamaHost?: string;
|
||||||
|
fetch?: typeof fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OllamaEmbedResponse {
|
interface OllamaEmbedResponse {
|
||||||
embeddings: number[][];
|
embeddings: number[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeOllamaEmbeddings(config: OllamaEmbeddingsConfig): EmbeddingsServiceShape {
|
||||||
|
const defaultModel = config.model ?? "mxbai-embed-large";
|
||||||
|
const ollamaHost =
|
||||||
|
config.ollamaHost ??
|
||||||
|
process.env.OLLAMA_URL ??
|
||||||
|
process.env.OLLAMA_HOST ??
|
||||||
|
"http://localhost:11434";
|
||||||
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
||||||
|
|
||||||
|
return {
|
||||||
|
embed: Effect.fn("OllamaEmbeddings.embed")((texts: ReadonlyArray<string>, model?: string) => {
|
||||||
|
if (texts.length === 0) {
|
||||||
|
return Effect.succeed([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useModel = model ?? defaultModel;
|
||||||
|
const url = `${ollamaHost}/api/embed`;
|
||||||
|
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const body = yield* S.encodeUnknownEffect(S.UnknownFromJsonString)({
|
||||||
|
model: useModel,
|
||||||
|
input: Array.from(texts),
|
||||||
|
}).pipe(
|
||||||
|
Effect.mapError((error) => embeddingsError("ollama.encode-request", error, "ollama")),
|
||||||
|
);
|
||||||
|
|
||||||
|
return yield* Effect.tryPromise({
|
||||||
|
try: async () => {
|
||||||
|
const response = await fetchImpl(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorBody = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`Ollama embeddings request failed (${response.status}): ${errorBody}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as OllamaEmbedResponse;
|
||||||
|
return data.embeddings;
|
||||||
|
},
|
||||||
|
catch: (error) => embeddingsError("ollama.embed", error, "ollama"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OllamaEmbeddingsLive(config: OllamaEmbeddingsConfig): Layer.Layer<Embeddings> {
|
||||||
|
return Layer.succeed(
|
||||||
|
Embeddings,
|
||||||
|
Embeddings.of(makeOllamaEmbeddings(config)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export class OllamaEmbeddingsProcessor extends EmbeddingsService {
|
export class OllamaEmbeddingsProcessor extends EmbeddingsService {
|
||||||
private defaultModel: string;
|
private readonly embeddings: EmbeddingsServiceShape;
|
||||||
private ollamaHost: string;
|
|
||||||
|
|
||||||
constructor(config: OllamaEmbeddingsConfig) {
|
constructor(config: OllamaEmbeddingsConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
|
this.embeddings = makeOllamaEmbeddings(config);
|
||||||
this.defaultModel = config.model ?? "mxbai-embed-large";
|
|
||||||
this.ollamaHost =
|
|
||||||
config.ollamaHost ??
|
|
||||||
process.env.OLLAMA_URL ??
|
|
||||||
process.env.OLLAMA_HOST ??
|
|
||||||
"http://localhost:11434";
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[OllamaEmbeddings] Initialized (host=${this.ollamaHost}, model=${this.defaultModel})`,
|
`[OllamaEmbeddings] Initialized (host=${config.ollamaHost ?? process.env.OLLAMA_URL ?? process.env.OLLAMA_HOST ?? "http://localhost:11434"}, model=${config.model ?? "mxbai-embed-large"})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onEmbeddings(texts: string[], model?: string): Promise<number[][]> {
|
override startEffect() {
|
||||||
if (!texts || texts.length === 0) {
|
return super.startEffect().pipe(
|
||||||
return [];
|
Effect.provideService(Embeddings, Embeddings.of(this.embeddings)),
|
||||||
}
|
);
|
||||||
|
|
||||||
const useModel = model ?? this.defaultModel;
|
|
||||||
|
|
||||||
const url = `${this.ollamaHost}/api/embed`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: useModel,
|
|
||||||
input: texts,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const body = await response.text();
|
|
||||||
throw new Error(
|
|
||||||
`Ollama embeddings request failed (${response.status}): ${body}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as OllamaEmbedResponse;
|
|
||||||
|
|
||||||
return data.embeddings;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const program = makeProcessorProgram({
|
||||||
|
id: "embeddings",
|
||||||
|
make: (config) => new OllamaEmbeddingsProcessor(config),
|
||||||
|
});
|
||||||
|
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
await OllamaEmbeddingsProcessor.launch("embeddings");
|
await OllamaEmbeddingsProcessor.launch("embeddings");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue