From aab4183d6516bd8491a1fe4e34e368117a8e40e1 Mon Sep 17 00:00:00 2001 From: Louis Young <63398145+louisyoungx@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:13:24 +0800 Subject: [PATCH] feat(runtime): flowgram workflow node.js runtime (#304) * feat(runtime): init nodejs runtime * feat(runtime): init folder struct * feat(runtime): interface & test * feat(runtime): basic api & schema interfaces * feat(runtime): init runtime model framework * feat(runtime): create document & node entities * feat(runtime): runtime engine basic execute logic * feat(runtime): node add variable data * refactor(runtime): split to sub domains * test(runtime): document module test * feat(runtime): variable store * feat(runtime): workflow runtime executor * chore(demo): reset initial data * feat(runtime): workflow runtime branch logic * feat(runtime): workflow runtime access to ai model * feat(runtime): workflow runtime data all add to context * feat(runtime): workflow runtime invoke record snaphots * feat(runtime): workflow runtime status * feat(runtime): main api request processing chain * chore(demo): reset initial data * refactor(runtime): types move to interface package * feat(runtime): router access api defines & interfaces * feat(runtime): standardize api register & gen api docs * feat(runtime): create snapshot before node execute * fix(sub-canvas): tips cannot close * chore(demo): reset initial data * feat(demo): make node schema runnable * feat(demo): access test run * feat(runtime): runtime core can run in both browser & server env * fix(runtime): condition value empty issue * feat(runtime): beautify structure data view * feat(demo): test run sidesheet * chore(demo): test run sidesheet button fixed * feat(demo): running node show flowing line * chore(demo): hide node result overflow * chore(demo): reset initial data * feat(runtime): workflow runtime support loop node * fix(container): sub canvas height issue * feat(demo): test run multiple result render * test(runtime): enbale test coverage * refactor(runtime): interface folders structure * refactor(runtime): core folders structure * refactor(runtime): core export apis & access to router * feat(demo): runtime plugin * feat(runtime): server add try-catch protection * fix(runtime): node process reset end time * chore: format json * chore: rush update * refactor(demo): running service move to runtime-plugin as built-in runtime service * fix(runtime): build error * test(runtime): disable nodejs test * fix(demo): test run result key indent width --- .vscode/settings.json | 13 +- apps/demo-free-layout/package.json | 2 + .../src/components/base-node/index.tsx | 2 + .../testrun/node-status-bar/group/index.css | 8 + .../testrun/node-status-bar/group/index.tsx | 61 + .../testrun/node-status-bar/header/index.tsx | 52 + .../testrun/node-status-bar/header/style.ts | 56 + .../testrun/node-status-bar/icon/success.tsx | 32 + .../testrun/node-status-bar/icon/warning.tsx | 22 + .../testrun/node-status-bar/index.tsx | 42 + .../testrun/node-status-bar/render/index.css | 14 + .../testrun/node-status-bar/render/index.tsx | 233 ++++ .../testrun/node-status-bar/viewer/index.css | 137 ++ .../testrun/node-status-bar/viewer/index.tsx | 154 +++ .../testrun/testrun-button/index.tsx | 78 ++ .../testrun/testrun-sidesheet/index.tsx | 138 ++ .../src/components/tools/index.tsx | 6 +- .../src/components/tools/run.tsx | 6 +- .../form-components/form-outputs/index.tsx | 12 +- .../src/hooks/use-editor-props.tsx | 17 +- apps/demo-free-layout/src/initial-data.ts | 401 +++--- .../src/nodes/end/form-meta.tsx | 6 +- apps/demo-free-layout/src/nodes/llm/index.ts | 22 +- apps/demo-free-layout/src/nodes/loop/index.ts | 4 +- .../src/nodes/loop/loop-form-render.tsx | 3 +- apps/demo-free-layout/src/plugins/index.ts | 1 + .../runtime-plugin/browser-client/index.ts | 17 + .../runtime-plugin/create-runtime-plugin.ts | 23 + .../src/plugins/runtime-plugin/index.ts | 2 + .../runtime-plugin/runtime-service/index.ts | 172 +++ .../runtime-plugin/server-client/constant.ts | 7 + .../runtime-plugin/server-client/index.ts | 134 ++ .../runtime-plugin/server-client/type.ts | 4 + .../src/plugins/runtime-plugin/type.ts | 16 + apps/demo-free-layout/src/services/index.ts | 1 - .../src/services/running-service.ts | 47 - common/config/rush/pnpm-lock.yaml | 1232 ++++++++++++++++- cspell.json | 6 +- .../sub-canvas/components/render/index.tsx | 11 +- .../src/sub-canvas/components/tips/style.ts | 1 + packages/runtime/interface/.eslintrc.cjs | 6 + packages/runtime/interface/package.json | 44 + .../runtime/interface/src/api/constant.ts | 22 + packages/runtime/interface/src/api/define.ts | 19 + packages/runtime/interface/src/api/index.ts | 10 + packages/runtime/interface/src/api/schema.ts | 31 + .../interface/src/api/server-info/index.ts | 31 + .../interface/src/api/task-cancel/index.ts | 27 + .../interface/src/api/task-report/index.ts | 31 + .../interface/src/api/task-result/index.ts | 25 + .../interface/src/api/task-run/index.ts | 31 + packages/runtime/interface/src/api/type.ts | 18 + .../interface/src/api/validation/index.ts | 43 + .../runtime/interface/src/client/index.ts | 18 + packages/runtime/interface/src/index.ts | 5 + .../runtime/interface/src/node/constant.ts | 11 + .../runtime/interface/src/node/end/index.ts | 13 + packages/runtime/interface/src/node/index.ts | 4 + .../runtime/interface/src/node/llm/index.ts | 20 + .../runtime/interface/src/node/start/index.ts | 10 + .../interface/src/runtime/base/index.ts | 3 + .../src/runtime/base/inputs-outputs.ts | 2 + .../interface/src/runtime/base/invoke.ts | 9 + .../src/runtime/base/value-object.ts | 1 + .../interface/src/runtime/container/index.ts | 5 + .../interface/src/runtime/context/index.ts | 25 + .../src/runtime/document/document.ts | 14 + .../interface/src/runtime/document/edge.ts | 16 + .../interface/src/runtime/document/index.ts | 4 + .../interface/src/runtime/document/node.ts | 41 + .../interface/src/runtime/document/port.ts | 16 + .../interface/src/runtime/engine/index.ts | 16 + .../src/runtime/executor/executor.ts | 8 + .../interface/src/runtime/executor/index.ts | 7 + .../src/runtime/executor/node-executor.ts | 26 + .../runtime/interface/src/runtime/index.ts | 14 + .../interface/src/runtime/io-center/index.ts | 17 + .../interface/src/runtime/reporter/index.ts | 23 + .../interface/src/runtime/snapshot/index.ts | 2 + .../src/runtime/snapshot/snapshot-center.ts | 10 + .../src/runtime/snapshot/snapshot.ts | 21 + .../interface/src/runtime/state/index.ts | 20 + .../interface/src/runtime/status/index.ts | 33 + .../interface/src/runtime/task/index.ts | 14 + .../interface/src/runtime/validation/index.ts | 12 + .../interface/src/runtime/variable/index.ts | 44 + .../runtime/interface/src/schema/constant.ts | 14 + packages/runtime/interface/src/schema/edge.ts | 6 + .../runtime/interface/src/schema/index.ts | 8 + .../interface/src/schema/json-schema.ts | 33 + .../runtime/interface/src/schema/node-meta.ts | 6 + packages/runtime/interface/src/schema/node.ts | 19 + .../runtime/interface/src/schema/value.ts | 29 + .../runtime/interface/src/schema/workflow.ts | 7 + packages/runtime/interface/src/schema/xy.ts | 6 + packages/runtime/interface/tsconfig.json | 29 + packages/runtime/js-core/.eslintrc.cjs | 6 + packages/runtime/js-core/package.json | 53 + packages/runtime/js-core/src/api/index.ts | 17 + .../runtime/js-core/src/api/task-cancel.ts | 13 + .../runtime/js-core/src/api/task-report.ts | 21 + .../runtime/js-core/src/api/task-result.ts | 10 + packages/runtime/js-core/src/api/task-run.ts | 17 + .../runtime/js-core/src/application/index.ts | 1 + .../js-core/src/application/workflow.ts | 76 + .../js-core/src/domain/__tests__/config.ts | 1 + .../src/domain/__tests__/executor/index.ts | 13 + .../src/domain/__tests__/executor/llm.ts | 18 + .../__tests__/schemas/basic-llm.test.ts | 78 ++ .../src/domain/__tests__/schemas/basic-llm.ts | 169 +++ .../domain/__tests__/schemas/basic.test.ts | 79 ++ .../src/domain/__tests__/schemas/basic.ts | 177 +++ .../domain/__tests__/schemas/branch.test.ts | 184 +++ .../src/domain/__tests__/schemas/branch.ts | 287 ++++ .../src/domain/__tests__/schemas/index.ts | 13 + .../src/domain/__tests__/schemas/loop.test.ts | 189 +++ .../src/domain/__tests__/schemas/loop.ts | 164 +++ .../src/domain/__tests__/schemas/two-llm.ts | 267 ++++ .../js-core/src/domain/__tests__/setup.ts | 8 + .../src/domain/__tests__/utils/index.ts | 1 + .../src/domain/__tests__/utils/snapshot.ts | 16 + .../src/domain/container/index.test.ts | 43 + .../js-core/src/domain/container/index.ts | 46 + .../js-core/src/domain/context/index.ts | 116 ++ .../document/document/create-store.test.ts | 109 ++ .../domain/document/document/create-store.ts | 133 ++ .../document/document/flat-schema.test.ts | 73 + .../domain/document/document/flat-schema.ts | 73 + .../domain/document/document/index.test.ts | 86 ++ .../src/domain/document/document/index.ts | 72 + .../domain/document/entity/edge/index.test.ts | 98 ++ .../src/domain/document/entity/edge/index.ts | 49 + .../src/domain/document/entity/index.ts | 3 + .../domain/document/entity/node/index.test.ts | 152 ++ .../src/domain/document/entity/node/index.ts | 113 ++ .../domain/document/entity/port/index.test.ts | 74 + .../src/domain/document/entity/port/index.ts | 33 + .../js-core/src/domain/document/index.ts | 1 + .../js-core/src/domain/engine/index.test.ts | 53 + .../js-core/src/domain/engine/index.ts | 133 ++ .../js-core/src/domain/executor/index.ts | 33 + .../js-core/src/domain/io-center/index.ts | 36 + .../js-core/src/domain/report/index.ts | 1 + .../report/report-value-object/index.ts | 10 + .../src/domain/report/reporter/index.ts | 49 + .../js-core/src/domain/snapshot/index.ts | 1 + .../domain/snapshot/snapshot-center/index.ts | 44 + .../domain/snapshot/snapshot-entity/index.ts | 35 + .../runtime/js-core/src/domain/state/index.ts | 134 ++ .../js-core/src/domain/status/index.ts | 1 + .../src/domain/status/status-center/index.ts | 50 + .../src/domain/status/status-entity/index.ts | 91 ++ .../runtime/js-core/src/domain/task/index.ts | 35 + .../js-core/src/domain/validation/index.ts | 18 + .../js-core/src/domain/variable/index.ts | 1 + .../variable/variable-store/index.test.ts | 201 +++ .../domain/variable/variable-store/index.ts | 135 ++ .../variable/variable-value-object/index.ts | 10 + packages/runtime/js-core/src/index.ts | 1 + .../js-core/src/infrastructure/index.ts | 1 + .../js-core/src/infrastructure/utils/delay.ts | 1 + .../js-core/src/infrastructure/utils/index.ts | 3 + .../src/infrastructure/utils/runtime-type.ts | 60 + .../js-core/src/infrastructure/utils/uuid.ts | 3 + .../src/nodes/condition/handlers/array.ts | 16 + .../src/nodes/condition/handlers/boolean.ts | 38 + .../src/nodes/condition/handlers/index.ts | 19 + .../src/nodes/condition/handlers/null.ts | 19 + .../src/nodes/condition/handlers/number.ts | 48 + .../src/nodes/condition/handlers/object.ts | 16 + .../src/nodes/condition/handlers/string.ts | 40 + .../js-core/src/nodes/condition/index.ts | 82 ++ .../js-core/src/nodes/condition/rules.ts | 63 + .../js-core/src/nodes/condition/type.ts | 50 + .../runtime/js-core/src/nodes/end/index.ts | 17 + packages/runtime/js-core/src/nodes/index.ts | 15 + .../runtime/js-core/src/nodes/llm/index.ts | 69 + .../runtime/js-core/src/nodes/loop/index.ts | 77 ++ .../runtime/js-core/src/nodes/start/index.ts | 16 + packages/runtime/js-core/tsconfig.json | 26 + packages/runtime/js-core/vitest.config.ts | 39 + packages/runtime/nodejs/.eslintignore | 1 + packages/runtime/nodejs/.eslintrc.cjs | 24 + packages/runtime/nodejs/.gitignore | 41 + packages/runtime/nodejs/README.md | 16 + packages/runtime/nodejs/package.json | 62 + packages/runtime/nodejs/src/api/create-api.ts | 63 + packages/runtime/nodejs/src/api/index.ts | 16 + packages/runtime/nodejs/src/api/trpc.ts | 16 + packages/runtime/nodejs/src/api/type.ts | 9 + packages/runtime/nodejs/src/config/index.ts | 13 + packages/runtime/nodejs/src/index.ts | 8 + packages/runtime/nodejs/src/server/context.ts | 8 + packages/runtime/nodejs/src/server/docs.ts | 14 + packages/runtime/nodejs/src/server/index.ts | 89 ++ packages/runtime/nodejs/src/server/type.ts | 8 + packages/runtime/nodejs/tsconfig.json | 48 + packages/runtime/nodejs/vitest.config.ts | 40 + rush.json | 364 +++-- 199 files changed, 9571 insertions(+), 385 deletions(-) create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/group/index.css create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/group/index.tsx create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/header/index.tsx create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/header/style.ts create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/icon/success.tsx create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/icon/warning.tsx create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/index.tsx create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/render/index.css create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/render/index.tsx create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/viewer/index.css create mode 100644 apps/demo-free-layout/src/components/testrun/node-status-bar/viewer/index.tsx create mode 100644 apps/demo-free-layout/src/components/testrun/testrun-button/index.tsx create mode 100644 apps/demo-free-layout/src/components/testrun/testrun-sidesheet/index.tsx create mode 100644 apps/demo-free-layout/src/plugins/runtime-plugin/browser-client/index.ts create mode 100644 apps/demo-free-layout/src/plugins/runtime-plugin/create-runtime-plugin.ts create mode 100644 apps/demo-free-layout/src/plugins/runtime-plugin/index.ts create mode 100644 apps/demo-free-layout/src/plugins/runtime-plugin/runtime-service/index.ts create mode 100644 apps/demo-free-layout/src/plugins/runtime-plugin/server-client/constant.ts create mode 100644 apps/demo-free-layout/src/plugins/runtime-plugin/server-client/index.ts create mode 100644 apps/demo-free-layout/src/plugins/runtime-plugin/server-client/type.ts create mode 100644 apps/demo-free-layout/src/plugins/runtime-plugin/type.ts delete mode 100644 apps/demo-free-layout/src/services/running-service.ts create mode 100644 packages/runtime/interface/.eslintrc.cjs create mode 100644 packages/runtime/interface/package.json create mode 100644 packages/runtime/interface/src/api/constant.ts create mode 100644 packages/runtime/interface/src/api/define.ts create mode 100644 packages/runtime/interface/src/api/index.ts create mode 100644 packages/runtime/interface/src/api/schema.ts create mode 100644 packages/runtime/interface/src/api/server-info/index.ts create mode 100644 packages/runtime/interface/src/api/task-cancel/index.ts create mode 100644 packages/runtime/interface/src/api/task-report/index.ts create mode 100644 packages/runtime/interface/src/api/task-result/index.ts create mode 100644 packages/runtime/interface/src/api/task-run/index.ts create mode 100644 packages/runtime/interface/src/api/type.ts create mode 100644 packages/runtime/interface/src/api/validation/index.ts create mode 100644 packages/runtime/interface/src/client/index.ts create mode 100644 packages/runtime/interface/src/index.ts create mode 100644 packages/runtime/interface/src/node/constant.ts create mode 100644 packages/runtime/interface/src/node/end/index.ts create mode 100644 packages/runtime/interface/src/node/index.ts create mode 100644 packages/runtime/interface/src/node/llm/index.ts create mode 100644 packages/runtime/interface/src/node/start/index.ts create mode 100644 packages/runtime/interface/src/runtime/base/index.ts create mode 100644 packages/runtime/interface/src/runtime/base/inputs-outputs.ts create mode 100644 packages/runtime/interface/src/runtime/base/invoke.ts create mode 100644 packages/runtime/interface/src/runtime/base/value-object.ts create mode 100644 packages/runtime/interface/src/runtime/container/index.ts create mode 100644 packages/runtime/interface/src/runtime/context/index.ts create mode 100644 packages/runtime/interface/src/runtime/document/document.ts create mode 100644 packages/runtime/interface/src/runtime/document/edge.ts create mode 100644 packages/runtime/interface/src/runtime/document/index.ts create mode 100644 packages/runtime/interface/src/runtime/document/node.ts create mode 100644 packages/runtime/interface/src/runtime/document/port.ts create mode 100644 packages/runtime/interface/src/runtime/engine/index.ts create mode 100644 packages/runtime/interface/src/runtime/executor/executor.ts create mode 100644 packages/runtime/interface/src/runtime/executor/index.ts create mode 100644 packages/runtime/interface/src/runtime/executor/node-executor.ts create mode 100644 packages/runtime/interface/src/runtime/index.ts create mode 100644 packages/runtime/interface/src/runtime/io-center/index.ts create mode 100644 packages/runtime/interface/src/runtime/reporter/index.ts create mode 100644 packages/runtime/interface/src/runtime/snapshot/index.ts create mode 100644 packages/runtime/interface/src/runtime/snapshot/snapshot-center.ts create mode 100644 packages/runtime/interface/src/runtime/snapshot/snapshot.ts create mode 100644 packages/runtime/interface/src/runtime/state/index.ts create mode 100644 packages/runtime/interface/src/runtime/status/index.ts create mode 100644 packages/runtime/interface/src/runtime/task/index.ts create mode 100644 packages/runtime/interface/src/runtime/validation/index.ts create mode 100644 packages/runtime/interface/src/runtime/variable/index.ts create mode 100644 packages/runtime/interface/src/schema/constant.ts create mode 100644 packages/runtime/interface/src/schema/edge.ts create mode 100644 packages/runtime/interface/src/schema/index.ts create mode 100644 packages/runtime/interface/src/schema/json-schema.ts create mode 100644 packages/runtime/interface/src/schema/node-meta.ts create mode 100644 packages/runtime/interface/src/schema/node.ts create mode 100644 packages/runtime/interface/src/schema/value.ts create mode 100644 packages/runtime/interface/src/schema/workflow.ts create mode 100644 packages/runtime/interface/src/schema/xy.ts create mode 100644 packages/runtime/interface/tsconfig.json create mode 100644 packages/runtime/js-core/.eslintrc.cjs create mode 100644 packages/runtime/js-core/package.json create mode 100644 packages/runtime/js-core/src/api/index.ts create mode 100644 packages/runtime/js-core/src/api/task-cancel.ts create mode 100644 packages/runtime/js-core/src/api/task-report.ts create mode 100644 packages/runtime/js-core/src/api/task-result.ts create mode 100644 packages/runtime/js-core/src/api/task-run.ts create mode 100644 packages/runtime/js-core/src/application/index.ts create mode 100644 packages/runtime/js-core/src/application/workflow.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/config.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/executor/index.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/executor/llm.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/schemas/basic-llm.test.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/schemas/basic-llm.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/schemas/basic.test.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/schemas/basic.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/schemas/branch.test.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/schemas/branch.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/schemas/index.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/schemas/loop.test.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/schemas/loop.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/schemas/two-llm.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/setup.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/utils/index.ts create mode 100644 packages/runtime/js-core/src/domain/__tests__/utils/snapshot.ts create mode 100644 packages/runtime/js-core/src/domain/container/index.test.ts create mode 100644 packages/runtime/js-core/src/domain/container/index.ts create mode 100644 packages/runtime/js-core/src/domain/context/index.ts create mode 100644 packages/runtime/js-core/src/domain/document/document/create-store.test.ts create mode 100644 packages/runtime/js-core/src/domain/document/document/create-store.ts create mode 100644 packages/runtime/js-core/src/domain/document/document/flat-schema.test.ts create mode 100644 packages/runtime/js-core/src/domain/document/document/flat-schema.ts create mode 100644 packages/runtime/js-core/src/domain/document/document/index.test.ts create mode 100644 packages/runtime/js-core/src/domain/document/document/index.ts create mode 100644 packages/runtime/js-core/src/domain/document/entity/edge/index.test.ts create mode 100644 packages/runtime/js-core/src/domain/document/entity/edge/index.ts create mode 100644 packages/runtime/js-core/src/domain/document/entity/index.ts create mode 100644 packages/runtime/js-core/src/domain/document/entity/node/index.test.ts create mode 100644 packages/runtime/js-core/src/domain/document/entity/node/index.ts create mode 100644 packages/runtime/js-core/src/domain/document/entity/port/index.test.ts create mode 100644 packages/runtime/js-core/src/domain/document/entity/port/index.ts create mode 100644 packages/runtime/js-core/src/domain/document/index.ts create mode 100644 packages/runtime/js-core/src/domain/engine/index.test.ts create mode 100644 packages/runtime/js-core/src/domain/engine/index.ts create mode 100644 packages/runtime/js-core/src/domain/executor/index.ts create mode 100644 packages/runtime/js-core/src/domain/io-center/index.ts create mode 100644 packages/runtime/js-core/src/domain/report/index.ts create mode 100644 packages/runtime/js-core/src/domain/report/report-value-object/index.ts create mode 100644 packages/runtime/js-core/src/domain/report/reporter/index.ts create mode 100644 packages/runtime/js-core/src/domain/snapshot/index.ts create mode 100644 packages/runtime/js-core/src/domain/snapshot/snapshot-center/index.ts create mode 100644 packages/runtime/js-core/src/domain/snapshot/snapshot-entity/index.ts create mode 100644 packages/runtime/js-core/src/domain/state/index.ts create mode 100644 packages/runtime/js-core/src/domain/status/index.ts create mode 100644 packages/runtime/js-core/src/domain/status/status-center/index.ts create mode 100644 packages/runtime/js-core/src/domain/status/status-entity/index.ts create mode 100644 packages/runtime/js-core/src/domain/task/index.ts create mode 100644 packages/runtime/js-core/src/domain/validation/index.ts create mode 100644 packages/runtime/js-core/src/domain/variable/index.ts create mode 100644 packages/runtime/js-core/src/domain/variable/variable-store/index.test.ts create mode 100644 packages/runtime/js-core/src/domain/variable/variable-store/index.ts create mode 100644 packages/runtime/js-core/src/domain/variable/variable-value-object/index.ts create mode 100644 packages/runtime/js-core/src/index.ts create mode 100644 packages/runtime/js-core/src/infrastructure/index.ts create mode 100644 packages/runtime/js-core/src/infrastructure/utils/delay.ts create mode 100644 packages/runtime/js-core/src/infrastructure/utils/index.ts create mode 100644 packages/runtime/js-core/src/infrastructure/utils/runtime-type.ts create mode 100644 packages/runtime/js-core/src/infrastructure/utils/uuid.ts create mode 100644 packages/runtime/js-core/src/nodes/condition/handlers/array.ts create mode 100644 packages/runtime/js-core/src/nodes/condition/handlers/boolean.ts create mode 100644 packages/runtime/js-core/src/nodes/condition/handlers/index.ts create mode 100644 packages/runtime/js-core/src/nodes/condition/handlers/null.ts create mode 100644 packages/runtime/js-core/src/nodes/condition/handlers/number.ts create mode 100644 packages/runtime/js-core/src/nodes/condition/handlers/object.ts create mode 100644 packages/runtime/js-core/src/nodes/condition/handlers/string.ts create mode 100644 packages/runtime/js-core/src/nodes/condition/index.ts create mode 100644 packages/runtime/js-core/src/nodes/condition/rules.ts create mode 100644 packages/runtime/js-core/src/nodes/condition/type.ts create mode 100644 packages/runtime/js-core/src/nodes/end/index.ts create mode 100644 packages/runtime/js-core/src/nodes/index.ts create mode 100644 packages/runtime/js-core/src/nodes/llm/index.ts create mode 100644 packages/runtime/js-core/src/nodes/loop/index.ts create mode 100644 packages/runtime/js-core/src/nodes/start/index.ts create mode 100644 packages/runtime/js-core/tsconfig.json create mode 100644 packages/runtime/js-core/vitest.config.ts create mode 100644 packages/runtime/nodejs/.eslintignore create mode 100644 packages/runtime/nodejs/.eslintrc.cjs create mode 100644 packages/runtime/nodejs/.gitignore create mode 100644 packages/runtime/nodejs/README.md create mode 100644 packages/runtime/nodejs/package.json create mode 100644 packages/runtime/nodejs/src/api/create-api.ts create mode 100644 packages/runtime/nodejs/src/api/index.ts create mode 100644 packages/runtime/nodejs/src/api/trpc.ts create mode 100644 packages/runtime/nodejs/src/api/type.ts create mode 100644 packages/runtime/nodejs/src/config/index.ts create mode 100644 packages/runtime/nodejs/src/index.ts create mode 100644 packages/runtime/nodejs/src/server/context.ts create mode 100644 packages/runtime/nodejs/src/server/docs.ts create mode 100644 packages/runtime/nodejs/src/server/index.ts create mode 100644 packages/runtime/nodejs/src/server/type.ts create mode 100644 packages/runtime/nodejs/tsconfig.json create mode 100644 packages/runtime/nodejs/vitest.config.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 5fdf1298..3527fb8c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -69,7 +69,10 @@ }, "search.useIgnoreFiles": true, // - "editor.rulers": [80, 120], + "editor.rulers": [ + 80, + 120 + ], "files.eol": "\n", "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, @@ -97,7 +100,6 @@ "scss.validate": false, "less.validate": false, "emmet.triggerExpansionOnTab": true, - "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, @@ -108,10 +110,10 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[json]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" + "editor.defaultFormatter": "vscode.json-language-features" }, "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.json-language-features" }, "[less]": { "editor.defaultFormatter": "vscode.css-language-features" @@ -125,9 +127,6 @@ "[typescriptreact]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, - "[scss]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, "[ignore]": { "editor.defaultFormatter": "foxundermoon.shell-format" }, diff --git a/apps/demo-free-layout/package.json b/apps/demo-free-layout/package.json index 3201de69..685afac4 100644 --- a/apps/demo-free-layout/package.json +++ b/apps/demo-free-layout/package.json @@ -30,6 +30,7 @@ "dependencies": { "@douyinfe/semi-icons": "^2.80.0", "@douyinfe/semi-ui": "^2.80.0", + "@flowgram.ai/runtime-interface": "workspace:*", "@flowgram.ai/free-layout-editor": "workspace:*", "@flowgram.ai/free-snap-plugin": "workspace:*", "@flowgram.ai/free-lines-plugin": "workspace:*", @@ -38,6 +39,7 @@ "@flowgram.ai/free-container-plugin": "workspace:*", "@flowgram.ai/free-group-plugin": "workspace:*", "@flowgram.ai/form-materials": "workspace:*", + "@flowgram.ai/runtime-js": "workspace:*", "lodash-es": "^4.17.21", "nanoid": "^4.0.2", "react": "^18", diff --git a/apps/demo-free-layout/src/components/base-node/index.tsx b/apps/demo-free-layout/src/components/base-node/index.tsx index 83b386d8..7c164b59 100644 --- a/apps/demo-free-layout/src/components/base-node/index.tsx +++ b/apps/demo-free-layout/src/components/base-node/index.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { FlowNodeEntity, useNodeRender } from '@flowgram.ai/free-layout-editor'; import { ConfigProvider } from '@douyinfe/semi-ui'; +import { NodeStatusBar } from '../testrun/node-status-bar'; import { NodeRenderContext } from '../../context'; import { ErrorIcon } from './styles'; import { NodeWrapper } from './node-wrapper'; @@ -32,6 +33,7 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => { {form?.state.invalid && } {form?.render()} + ); diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/group/index.css b/apps/demo-free-layout/src/components/testrun/node-status-bar/group/index.css new file mode 100644 index 00000000..a683fe8f --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/group/index.css @@ -0,0 +1,8 @@ +.node-status-group { + padding: 6px; + font-weight: 500; + color: #333; + font-size: 15px; + display: flex; + align-items: center; +} diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/group/index.tsx b/apps/demo-free-layout/src/components/testrun/node-status-bar/group/index.tsx new file mode 100644 index 00000000..dd6a7936 --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/group/index.tsx @@ -0,0 +1,61 @@ +import { FC, useState } from 'react'; + +import { IconSmallTriangleDown } from '@douyinfe/semi-icons'; + +import { DataStructureViewer } from '../viewer'; + +import './index.css'; +import { Tag } from '@douyinfe/semi-ui'; + +interface NodeStatusGroupProps { + title: string; + data: unknown; + optional?: boolean; + disableCollapse?: boolean; +} + +const isObjectHasContent = (obj: any = {}): boolean => Object.keys(obj).length > 0; + +export const NodeStatusGroup: FC = ({ + title, + data, + optional = false, + disableCollapse = false, +}) => { + const hasContent = isObjectHasContent(data); + const [isExpanded, setIsExpanded] = useState(true); + + if (optional && !hasContent) { + return null; + } + + return ( + <> +
hasContent && setIsExpanded(!isExpanded)}> + {!disableCollapse && ( + + )} + {title}: + {!hasContent && ( + + null + + )} +
+ {hasContent && isExpanded ? : null} + + ); +}; diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/header/index.tsx b/apps/demo-free-layout/src/components/testrun/node-status-bar/header/index.tsx new file mode 100644 index 00000000..6ce6075f --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/header/index.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; + +import { IconChevronDown } from '@douyinfe/semi-icons'; + +import { useNodeRenderContext } from '../../../../hooks'; +import { NodeStatusHeaderContentStyle, NodeStatusHeaderStyle } from './style'; + +interface NodeStatusBarProps { + header?: React.ReactNode; + defaultShowDetail?: boolean; + extraBtns?: React.ReactNode[]; +} + +export const NodeStatusHeader: React.FC> = ({ + header, + defaultShowDetail, + children, + extraBtns = [], +}) => { + const [showDetail, setShowDetail] = useState(defaultShowDetail); + const { selectNode } = useNodeRenderContext(); + + const handleToggleShowDetail = (e: React.MouseEvent) => { + e.stopPropagation(); + selectNode(e); + setShowDetail(!showDetail); + }; + + return ( + e.stopPropagation()} + > + e.stopPropagation()} + // 其他事件统一走点击事件,且也需要阻止冒泡 + onClick={handleToggleShowDetail} + > +
+ {header} + {extraBtns.length > 0 ? extraBtns : null} +
+
+ +
+
+ {showDetail ? children : null} +
+ ); +}; diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/header/style.ts b/apps/demo-free-layout/src/components/testrun/node-status-bar/header/style.ts new file mode 100644 index 00000000..e9cefa9b --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/header/style.ts @@ -0,0 +1,56 @@ +import styled from 'styled-components'; + +export const NodeStatusHeaderStyle = styled.div` + border: 1px solid rgba(68, 83, 130, 0.25); + border-radius: 8px; + background-color: #fff; + + position: absolute; + top: calc(100% + 8px); + left: 0; + + width: 100%; +`; + +export const NodeStatusHeaderContentStyle = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px; + + &-opened { + padding-bottom: 0; + } + + .status-title { + height: 24px; + display: flex; + align-items: center; + column-gap: 8px; + min-width: 0; + + :global { + .coz-tag { + height: 20px; + } + .semi-tag-content { + font-weight: 500; + line-height: 16px; + font-size: 12px; + } + .semi-tag-suffix-icon > div { + font-size: 14px; + } + } + } + .status-btns { + height: 24px; + display: flex; + align-items: center; + column-gap: 4px; + } + + .is-show-detail { + transform: rotate(180deg); + } +`; diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/icon/success.tsx b/apps/demo-free-layout/src/components/testrun/node-status-bar/icon/success.tsx new file mode 100644 index 00000000..ca2f3af6 --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/icon/success.tsx @@ -0,0 +1,32 @@ +interface Props { + className?: string; + style?: React.CSSProperties; +} + +export const IconSuccessFill = ({ className, style }: Props) => ( + + + + + + + + + + + +); diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/icon/warning.tsx b/apps/demo-free-layout/src/components/testrun/node-status-bar/icon/warning.tsx new file mode 100644 index 00000000..fa6108e6 --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/icon/warning.tsx @@ -0,0 +1,22 @@ +interface Props { + className?: string; + style?: React.CSSProperties; +} + +export const IconWarningFill = ({ className, style }: Props) => ( + + + +); diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/index.tsx b/apps/demo-free-layout/src/components/testrun/node-status-bar/index.tsx new file mode 100644 index 00000000..3f78b1c6 --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/index.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; + +import { NodeReport } from '@flowgram.ai/runtime-interface'; +import { useCurrentEntity, useService } from '@flowgram.ai/free-layout-editor'; + +import { WorkflowRuntimeService } from '../../../plugins/runtime-plugin/runtime-service'; +import { NodeStatusRender } from './render'; + +const useNodeReport = () => { + const node = useCurrentEntity(); + const [report, setReport] = useState(); + + const runtimeService = useService(WorkflowRuntimeService); + + useEffect(() => { + const reportDisposer = runtimeService.onNodeReportChange((nodeReport) => { + if (nodeReport.id !== node.id) { + return; + } + setReport(nodeReport); + }); + const resetDisposer = runtimeService.onReset(() => { + setReport(undefined); + }); + return () => { + reportDisposer.dispose(); + resetDisposer.dispose(); + }; + }, []); + + return report; +}; + +export const NodeStatusBar = () => { + const report = useNodeReport(); + + if (!report) { + return null; + } + + return ; +}; diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/render/index.css b/apps/demo-free-layout/src/components/testrun/node-status-bar/render/index.css new file mode 100644 index 00000000..e116c559 --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/render/index.css @@ -0,0 +1,14 @@ +.node-status-succeed { + background-color: rgba(105, 209, 140, 0.3); + color: rgba(0, 178, 60, 1); +} + +.node-status-processing { + background-color: rgba(153, 187, 255, 0.3); + color: rgba(61, 121, 242, 1); +} + +.node-status-failed { + background-color: rgba(255, 163, 171, 0.3); + color: rgba(229, 50, 65, 1); +} diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/render/index.tsx b/apps/demo-free-layout/src/components/testrun/node-status-bar/render/index.tsx new file mode 100644 index 00000000..4581ee06 --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/render/index.tsx @@ -0,0 +1,233 @@ +import { FC, useMemo, useState } from 'react'; + +import { NodeReport, WorkflowStatus } from '@flowgram.ai/runtime-interface'; +import { Tag, Button, Select } from '@douyinfe/semi-ui'; +import { IconSpin } from '@douyinfe/semi-icons'; + +import { IconWarningFill } from '../icon/warning'; +import { IconSuccessFill } from '../icon/success'; +import { NodeStatusHeader } from '../header'; +import './index.css'; +import { NodeStatusGroup } from '../group'; + +interface NodeStatusRenderProps { + report: NodeReport; +} + +const msToSeconds = (ms: number): string => (ms / 1000).toFixed(2) + 's'; +const displayCount = 6; + +export const NodeStatusRender: FC = ({ report }) => { + const { status: nodeStatus } = report; + const [currentSnapshotIndex, setCurrentSnapshotIndex] = useState(0); + + const snapshots = report.snapshots || []; + const currentSnapshot = snapshots[currentSnapshotIndex] || snapshots[0]; + + // 节点 5 个状态 + const isNodePending = nodeStatus === WorkflowStatus.Pending; + const isNodeProcessing = nodeStatus === WorkflowStatus.Processing; + const isNodeFailed = nodeStatus === WorkflowStatus.Failed; + const isNodeSucceed = nodeStatus === WorkflowStatus.Succeeded; + const isNodeCanceled = nodeStatus === WorkflowStatus.Canceled; + + const tagColor = useMemo(() => { + if (isNodeSucceed) { + return 'node-status-succeed'; + } + if (isNodeFailed) { + return 'node-status-failed'; + } + if (isNodeProcessing) { + return 'node-status-processing'; + } + }, [isNodeSucceed, isNodeFailed, isNodeProcessing]); + + const renderIcon = () => { + if (isNodeProcessing) { + return ( + + ); + } + if (isNodeSucceed) { + return ; + } + return ; + }; + const renderDesc = () => { + const getDesc = () => { + if (isNodeProcessing) { + return 'Running'; + } else if (isNodePending) { + return 'Run terminated'; + } else if (isNodeSucceed) { + return 'Succeed'; + } else if (isNodeFailed) { + return 'Failed'; + } else if (isNodeCanceled) { + return 'Canceled'; + } + }; + + const desc = getDesc(); + + return desc ?

{desc}

: null; + }; + const renderCost = () => ( + + {msToSeconds(report.timeCost)} + + ); + + const renderSnapshotNavigation = () => { + if (snapshots.length <= 1) { + return null; + } + + const count = ( +

+ Total: {snapshots.length} +

+ ); + + if (snapshots.length <= displayCount) { + return ( + <> + {count} +
+ {snapshots.map((_, index) => ( + + ))} +
+ + ); + } + + // 超过5个时,前5个显示为按钮,剩余的放在下拉选择中 + return ( + <> + {count} +
+ {snapshots.slice(0, displayCount).map((_, index) => ( + + ))} + +
+ + ); + }; + + if (!report) { + return null; + } + + return ( + + {renderIcon()} + {renderDesc()} + {renderCost()} + + } + > +
+ {renderSnapshotNavigation()} + + + + +
+
+ ); +}; diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/viewer/index.css b/apps/demo-free-layout/src/components/testrun/node-status-bar/viewer/index.css new file mode 100644 index 00000000..72625e64 --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/viewer/index.css @@ -0,0 +1,137 @@ +.node-status-data-structure-viewer { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + line-height: 1.5; + color: #333; + background: #fafafa; + border-radius: 6px; + padding: 12px 12px 12px 0; + margin: 12px; + border: 1px solid #e1e4e8; + overflow: hidden; +} + +.tree-node { + margin: 2px 0; +} + +.tree-node-header { + display: flex; + align-items: flex-start; + gap: 4px; + min-height: 20px; + padding: 2px 0; + border-radius: 3px; + transition: background-color 0.15s ease; +} + +.tree-node-header:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +.expand-button { + background: none; + border: none; + cursor: pointer; + font-size: 10px; + color: #666; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + transition: all 0.15s ease; + padding: 0; + margin: 0; +} + +.expand-button:hover { + background-color: rgba(0, 0, 0, 0.1); + color: #333; +} + +.expand-button.expanded { + transform: rotate(90deg); +} + +.expand-button.collapsed { + transform: rotate(0deg); +} + +.expand-placeholder { + width: 16px; + height: 16px; + display: inline-block; + flex-shrink: 0; +} + +.node-label { + color: #0969da; + font-weight: 500; + cursor: pointer; + user-select: auto; + margin-right: 4px; +} + +.node-label:hover { + text-decoration: underline; +} + +.node-value { + margin-left: 4px; +} + +.primitive-value-quote { + color: #8f8f8f; +} + +.primitive-value { + cursor: pointer; + user-select: all; + padding: 1px 3px; + border-radius: 3px; + transition: background-color 0.15s ease; +} + +.primitive-value:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.primitive-value.string { + color: #032f62; + background-color: rgba(3, 47, 98, 0.05); +} + +.primitive-value.number { + color: #005cc5; + background-color: rgba(0, 92, 197, 0.05); +} + +.primitive-value.boolean { + color: #e36209; + background-color: rgba(227, 98, 9, 0.05); +} + +.primitive-value.null, +.primitive-value.undefined { + color: #6a737d; + font-style: italic; + background-color: rgba(106, 115, 125, 0.05); +} + +.tree-node-children { + margin-left: 8px; + padding-left: 8px; + position: relative; +} + +.tree-node-children::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 1px; + background: #e1e4e8; +} diff --git a/apps/demo-free-layout/src/components/testrun/node-status-bar/viewer/index.tsx b/apps/demo-free-layout/src/components/testrun/node-status-bar/viewer/index.tsx new file mode 100644 index 00000000..10e319ac --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/node-status-bar/viewer/index.tsx @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; + +import './index.css'; +import { Toast } from '@douyinfe/semi-ui'; + +interface DataStructureViewerProps { + data: any; + level?: number; +} + +interface TreeNodeProps { + label: string; + value: any; + level: number; + isLast?: boolean; +} + +const TreeNode: React.FC = ({ label, value, level, isLast = false }) => { + const [isExpanded, setIsExpanded] = useState(true); + + const handleCopy = (text: string) => { + navigator.clipboard.writeText(text); + Toast.success('Copied'); + }; + + const isExpandable = (val: any) => + val !== null && + typeof val === 'object' && + ((Array.isArray(val) && val.length > 0) || + (!Array.isArray(val) && Object.keys(val).length > 0)); + + const renderPrimitiveValue = (val: any) => { + if (val === null) return null; + if (val === undefined) return undefined; + + switch (typeof val) { + case 'string': + return ( + + {'"'} + handleCopy(val)}> + {val} + + {'"'} + + ); + case 'number': + return ( + handleCopy(String(val))}> + {val} + + ); + case 'boolean': + return ( + handleCopy(val.toString())} + > + {val.toString()} + + ); + default: + return ( + handleCopy(String(val))}> + {String(val)} + + ); + } + }; + + const renderChildren = () => { + if (Array.isArray(value)) { + return value.map((item, index) => ( + + )); + } else { + const entries = Object.entries(value); + return entries.map(([key, val], index) => ( + + )); + } + }; + + return ( +
+
+ {isExpandable(value) ? ( + + ) : ( + + )} + + handleCopy( + JSON.stringify({ + [label]: value, + }) + ) + } + > + {label} + + {!isExpandable(value) && {renderPrimitiveValue(value)}} +
+ {isExpandable(value) && isExpanded && ( +
{renderChildren()}
+ )} +
+ ); +}; + +export const DataStructureViewer: React.FC = ({ data, level = 0 }) => { + if (data === null || data === undefined || typeof data !== 'object') { + return ( +
+ +
+ ); + } + + const entries = Object.entries(data); + + return ( +
+ {entries.map(([key, value], index) => ( + + ))} +
+ ); +}; diff --git a/apps/demo-free-layout/src/components/testrun/testrun-button/index.tsx b/apps/demo-free-layout/src/components/testrun/testrun-button/index.tsx new file mode 100644 index 00000000..c838f39f --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/testrun-button/index.tsx @@ -0,0 +1,78 @@ +import { useState, useEffect, useCallback } from 'react'; + +import { useClientContext, getNodeForm, FlowNodeEntity } from '@flowgram.ai/free-layout-editor'; +import { Button, Badge, SideSheet } from '@douyinfe/semi-ui'; +import { IconPlay } from '@douyinfe/semi-icons'; + +import { TestRunSideSheet } from '../testrun-sidesheet'; + +export function TestRunButton(props: { disabled: boolean }) { + const [errorCount, setErrorCount] = useState(0); + const clientContext = useClientContext(); + const [visible, setVisible] = useState(false); + + const updateValidateData = useCallback(() => { + const allForms = clientContext.document.getAllNodes().map((node) => getNodeForm(node)); + const count = allForms.filter((form) => form?.state.invalid).length; + setErrorCount(count); + }, [clientContext]); + + /** + * Validate all node and Save + */ + const onTestRun = useCallback(async () => { + const allForms = clientContext.document.getAllNodes().map((node) => getNodeForm(node)); + await Promise.all(allForms.map(async (form) => form?.validate())); + console.log('>>>>> save data: ', clientContext.document.toJSON()); + setVisible(true); + }, [clientContext]); + + /** + * Listen single node validate + */ + useEffect(() => { + const listenSingleNodeValidate = (node: FlowNodeEntity) => { + const form = getNodeForm(node); + if (form) { + const formValidateDispose = form.onValidate(() => updateValidateData()); + node.onDispose(() => formValidateDispose.dispose()); + } + }; + clientContext.document.getAllNodes().map((node) => listenSingleNodeValidate(node)); + const dispose = clientContext.document.onNodeCreate(({ node }) => + listenSingleNodeValidate(node) + ); + return () => dispose.dispose(); + }, [clientContext]); + + const button = + errorCount === 0 ? ( + + ) : ( + + + + ); + + return ( + <> + {button} + setVisible((v) => !v)} /> + + ); +} diff --git a/apps/demo-free-layout/src/components/testrun/testrun-sidesheet/index.tsx b/apps/demo-free-layout/src/components/testrun/testrun-sidesheet/index.tsx new file mode 100644 index 00000000..5b9dd297 --- /dev/null +++ b/apps/demo-free-layout/src/components/testrun/testrun-sidesheet/index.tsx @@ -0,0 +1,138 @@ +import { FC, useEffect, useState } from 'react'; + +import { WorkflowInputs, WorkflowOutputs } from '@flowgram.ai/runtime-interface'; +import { useService } from '@flowgram.ai/free-layout-editor'; +import { Button, JsonViewer, SideSheet } from '@douyinfe/semi-ui'; +import { IconPlay, IconSpin, IconStop } from '@douyinfe/semi-icons'; + +import { NodeStatusGroup } from '../node-status-bar/group'; +import { WorkflowRuntimeService } from '../../../plugins/runtime-plugin/runtime-service'; + +interface TestRunSideSheetProps { + visible: boolean; + onCancel: () => void; +} + +export const TestRunSideSheet: FC = ({ visible, onCancel }) => { + const runtimeService = useService(WorkflowRuntimeService); + const [isRunning, setRunning] = useState(false); + const [value, setValue] = useState(`{}`); + const [error, setError] = useState(); + const [result, setResult] = useState< + | { + inputs: WorkflowInputs; + outputs: WorkflowOutputs; + } + | undefined + >(); + + const onTestRun = async () => { + if (isRunning) { + await runtimeService.taskCancel(); + return; + } + setResult(undefined); + setError(undefined); + setRunning(true); + try { + await runtimeService.taskRun(value); + } catch (e: any) { + setError(e.message); + } + }; + + const onClose = async () => { + await runtimeService.taskCancel(); + setValue(`{}`); + setRunning(false); + onCancel(); + }; + + useEffect(() => { + const disposer = runtimeService.onTerminated(({ result }) => { + setRunning(false); + setResult(result); + }); + return () => disposer.dispose(); + }, []); + + const renderRunning = ( +
+ +
+ Running... +
+
+ ); + + const renderForm = ( +
+
+ Input +
+ +
+ {error} +
+ + + +
+ ); + + const renderButton = ( + + ); + + return ( + + {isRunning ? renderRunning : renderForm} + + ); +}; diff --git a/apps/demo-free-layout/src/components/tools/index.tsx b/apps/demo-free-layout/src/components/tools/index.tsx index 92aadf9f..9e33e20b 100644 --- a/apps/demo-free-layout/src/components/tools/index.tsx +++ b/apps/demo-free-layout/src/components/tools/index.tsx @@ -5,12 +5,11 @@ import { useClientContext } from '@flowgram.ai/free-layout-editor'; import { Tooltip, IconButton, Divider } from '@douyinfe/semi-ui'; import { IconUndo, IconRedo } from '@douyinfe/semi-icons'; +import { TestRunButton } from '../testrun/testrun-button'; import { AddNode } from '../add-node'; import { ZoomSelect } from './zoom-select'; import { SwitchLine } from './switch-line'; import { ToolContainer, ToolSection } from './styles'; -import { Save } from './save'; -import { Run } from './run'; import { Readonly } from './readonly'; import { MinimapSwitch } from './minimap-switch'; import { Minimap } from './minimap'; @@ -71,8 +70,7 @@ export const DemoTools = () => { - - + ); diff --git a/apps/demo-free-layout/src/components/tools/run.tsx b/apps/demo-free-layout/src/components/tools/run.tsx index 689856cb..d3b09b0a 100644 --- a/apps/demo-free-layout/src/components/tools/run.tsx +++ b/apps/demo-free-layout/src/components/tools/run.tsx @@ -3,17 +3,17 @@ import { useState } from 'react'; import { useService } from '@flowgram.ai/free-layout-editor'; import { Button } from '@douyinfe/semi-ui'; -import { RunningService } from '../../services'; +import { WorkflowRuntimeService } from '../../plugins/runtime-plugin/runtime-service'; /** * Run the simulation and highlight the lines */ export function Run() { const [isRunning, setRunning] = useState(false); - const runningService = useService(RunningService); + const runtimeService = useService(WorkflowRuntimeService); const onRun = async () => { setRunning(true); - await runningService.startRun(); + await runtimeService.taskRun('{}'); setRunning(false); }; return ( diff --git a/apps/demo-free-layout/src/form-components/form-outputs/index.tsx b/apps/demo-free-layout/src/form-components/form-outputs/index.tsx index 905a9a39..fb0abf23 100644 --- a/apps/demo-free-layout/src/form-components/form-outputs/index.tsx +++ b/apps/demo-free-layout/src/form-components/form-outputs/index.tsx @@ -1,3 +1,5 @@ +import { FC } from 'react'; + import { Field } from '@flowgram.ai/free-layout-editor'; import { TypeTag } from '../type-tag'; @@ -5,13 +7,17 @@ import { JsonSchema } from '../../typings'; import { useIsSidebar } from '../../hooks'; import { FormOutputsContainer } from './styles'; -export function FormOutputs() { +interface FormOutputsProps { + name?: string; +} + +export const FormOutputs: FC = ({ name = 'outputs' }) => { const isSidebar = useIsSidebar(); if (isSidebar) { return null; } return ( - name={'outputs'}> + name={name}> {({ field }) => { const properties = field.value?.properties; if (properties) { @@ -25,4 +31,4 @@ export function FormOutputs() { }} ); -} +}; diff --git a/apps/demo-free-layout/src/hooks/use-editor-props.tsx b/apps/demo-free-layout/src/hooks/use-editor-props.tsx index 34aff837..9e322eab 100644 --- a/apps/demo-free-layout/src/hooks/use-editor-props.tsx +++ b/apps/demo-free-layout/src/hooks/use-editor-props.tsx @@ -13,8 +13,9 @@ import { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin'; import { onDragLineEnd } from '../utils'; import { FlowNodeRegistry, FlowDocumentJSON } from '../typings'; import { shortcuts } from '../shortcuts'; -import { CustomService, RunningService } from '../services'; -import { createSyncVariablePlugin, createContextMenuPlugin } from '../plugins'; +import { CustomService } from '../services'; +import { WorkflowRuntimeService } from '../plugins/runtime-plugin/runtime-service'; +import { createSyncVariablePlugin, createRuntimePlugin, createContextMenuPlugin } from '../plugins'; import { defaultFormMeta } from '../nodes/default-form-meta'; import { WorkflowNodeType } from '../nodes'; import { SelectorBoxPopover } from '../components/selector-box-popover'; @@ -160,7 +161,7 @@ export function useEditorProps( /** * Running line */ - isFlowingLine: (ctx, line) => ctx.get(RunningService).isFlowingLine(line), + isFlowingLine: (ctx, line) => ctx.get(WorkflowRuntimeService).isFlowingLine(line), /** * Shortcuts @@ -171,7 +172,6 @@ export function useEditorProps( */ onBind: ({ bind }) => { bind(CustomService).toSelf().inSingletonScope(); - bind(RunningService).toSelf().inSingletonScope(); }, /** * Playground init @@ -264,6 +264,15 @@ export function useEditorProps( * ContextMenu plugin */ createContextMenuPlugin({}), + createRuntimePlugin({ + mode: 'browser', + // mode: 'server', + // serverConfig: { + // domain: 'localhost', + // port: 4000, + // protocol: 'http', + // }, + }), ], }), [] diff --git a/apps/demo-free-layout/src/initial-data.ts b/apps/demo-free-layout/src/initial-data.ts index 1bb98246..1f17e3cb 100644 --- a/apps/demo-free-layout/src/initial-data.ts +++ b/apps/demo-free-layout/src/initial-data.ts @@ -48,7 +48,7 @@ export const initialData: FlowDocumentJSON = { meta: { position: { x: 640, - y: 363.25, + y: 318.25, }, }, data: { @@ -86,13 +86,13 @@ export const initialData: FlowDocumentJSON = { type: 'end', meta: { position: { - x: 2220, + x: 2202.9953917050693, y: 381.75, }, }, data: { title: 'End', - outputs: { + inputs: { type: 'object', properties: { result: { @@ -102,160 +102,13 @@ export const initialData: FlowDocumentJSON = { }, }, }, - { - id: 'loop_H8M3U', - type: 'loop', - meta: { - position: { - x: 1020, - y: 547.96875, - }, - }, - data: { - title: 'Loop_2', - batchFor: { - type: 'ref', - content: ['start_0', 'array_obj'], - }, - outputs: { - type: 'object', - properties: { - result: { - type: 'string', - }, - }, - }, - }, - blocks: [ - { - id: 'llm_CBdCg', - type: 'llm', - meta: { - position: { - x: 180, - y: 0, - }, - }, - data: { - title: 'LLM_4', - inputsValues: { - modelType: { - type: 'constant', - content: 'gpt-3.5-turbo', - }, - temperature: { - type: 'constant', - content: 0.5, - }, - systemPrompt: { - type: 'constant', - content: 'You are an AI assistant.', - }, - prompt: { - type: 'constant', - content: '', - }, - }, - inputs: { - type: 'object', - required: ['modelType', 'temperature', 'prompt'], - properties: { - modelType: { - type: 'string', - }, - temperature: { - type: 'number', - }, - systemPrompt: { - type: 'string', - }, - prompt: { - type: 'string', - }, - }, - }, - outputs: { - type: 'object', - properties: { - result: { - type: 'string', - }, - }, - }, - }, - }, - { - id: 'llm_gZafu', - type: 'llm', - meta: { - position: { - x: 640, - y: 0, - }, - }, - data: { - title: 'LLM_5', - inputsValues: { - modelType: { - type: 'constant', - content: 'gpt-3.5-turbo', - }, - temperature: { - type: 'constant', - content: 0.5, - }, - systemPrompt: { - type: 'constant', - content: 'You are an AI assistant.', - }, - prompt: { - type: 'constant', - content: '', - }, - }, - inputs: { - type: 'object', - required: ['modelType', 'temperature', 'prompt'], - properties: { - modelType: { - type: 'string', - }, - temperature: { - type: 'number', - }, - systemPrompt: { - type: 'string', - }, - prompt: { - type: 'string', - }, - }, - }, - outputs: { - type: 'object', - properties: { - result: { - type: 'string', - }, - }, - }, - }, - }, - ], - edges: [ - { - sourceNodeID: 'llm_CBdCg', - targetNodeID: 'llm_gZafu', - }, - ], - }, { id: '159623', type: 'comment', meta: { position: { x: 640, - y: 522.46875, + y: 573.96875, }, }, data: { @@ -267,35 +120,42 @@ export const initialData: FlowDocumentJSON = { }, }, { - id: 'group_V-_st', - type: 'group', + id: 'loop_sGybT', + type: 'loop', meta: { position: { - x: 1020, - y: 96.25, + x: 1373.5714285714287, + y: 394.9758064516129, }, }, data: { - title: 'LLM_Group', - color: 'Violet', + title: 'Loop_1', }, blocks: [ { - id: 'llm_0', + id: 'llm_6aSyo', type: 'llm', meta: { position: { - x: 640, - y: 0, + x: -196.8663594470046, + y: 142.0046082949309, }, }, data: { - title: 'LLM_0', + title: 'LLM_3', inputsValues: { - modelType: { + modelName: { type: 'constant', content: 'gpt-3.5-turbo', }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, temperature: { type: 'constant', content: 0.5, @@ -311,9 +171,15 @@ export const initialData: FlowDocumentJSON = { }, inputs: { type: 'object', - required: ['modelType', 'temperature', 'prompt'], + required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'], properties: { - modelType: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { type: 'string', }, temperature: { @@ -338,21 +204,29 @@ export const initialData: FlowDocumentJSON = { }, }, { - id: 'llm_l_TcE', + id: 'llm_ZqKlP', type: 'llm', meta: { position: { - x: 180, - y: 0, + x: 253.1797235023041, + y: 142.00460829493088, }, }, data: { - title: 'LLM_1', + title: 'LLM_4', inputsValues: { - modelType: { + modelName: { type: 'constant', content: 'gpt-3.5-turbo', }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, temperature: { type: 'constant', content: 0.5, @@ -368,9 +242,15 @@ export const initialData: FlowDocumentJSON = { }, inputs: { type: 'object', - required: ['modelType', 'temperature', 'prompt'], + required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'], properties: { - modelType: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { type: 'string', }, temperature: { @@ -397,18 +277,179 @@ export const initialData: FlowDocumentJSON = { ], edges: [ { - sourceNodeID: 'llm_l_TcE', - targetNodeID: 'llm_0', + sourceNodeID: 'llm_6aSyo', + targetNodeID: 'llm_ZqKlP', + }, + ], + }, + { + id: 'group_5ci0o', + type: 'group', + meta: { + position: { + x: 0, + y: 0, + }, + }, + data: {}, + blocks: [ + { + id: 'llm_8--A3', + type: 'llm', + meta: { + position: { + x: 1177.8341013824886, + y: 19.25, + }, + }, + data: { + title: 'LLM_1', + inputsValues: { + modelName: { + type: 'constant', + content: 'gpt-3.5-turbo', + }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, + temperature: { + type: 'constant', + content: 0.5, + }, + systemPrompt: { + type: 'constant', + content: 'You are an AI assistant.', + }, + prompt: { + type: 'constant', + content: '', + }, + }, + inputs: { + type: 'object', + required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'], + properties: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { + type: 'string', + }, + temperature: { + type: 'number', + }, + systemPrompt: { + type: 'string', + }, + prompt: { + type: 'string', + }, + }, + }, + outputs: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + }, + }, }, { - sourceNodeID: 'llm_0', - targetNodeID: 'end_0', + id: 'llm_vTyMa', + type: 'llm', + meta: { + position: { + x: 1625.6221198156682, + y: 19.25, + }, + }, + data: { + title: 'LLM_2', + inputsValues: { + modelName: { + type: 'constant', + content: 'gpt-3.5-turbo', + }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, + temperature: { + type: 'constant', + content: 0.5, + }, + systemPrompt: { + type: 'constant', + content: 'You are an AI assistant.', + }, + prompt: { + type: 'constant', + content: '', + }, + }, + inputs: { + type: 'object', + required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'], + properties: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { + type: 'string', + }, + temperature: { + type: 'number', + }, + systemPrompt: { + type: 'string', + }, + prompt: { + type: 'string', + }, + }, + }, + outputs: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + }, + }, }, + ], + edges: [ { sourceNodeID: 'condition_0', - targetNodeID: 'llm_l_TcE', + targetNodeID: 'llm_8--A3', sourcePortID: 'if_0', }, + { + sourceNodeID: 'llm_8--A3', + targetNodeID: 'llm_vTyMa', + }, + { + sourceNodeID: 'llm_vTyMa', + targetNodeID: 'end_0', + }, ], }, ], @@ -419,20 +460,20 @@ export const initialData: FlowDocumentJSON = { }, { sourceNodeID: 'condition_0', - targetNodeID: 'llm_l_TcE', + targetNodeID: 'llm_8--A3', sourcePortID: 'if_0', }, { sourceNodeID: 'condition_0', - targetNodeID: 'loop_H8M3U', + targetNodeID: 'loop_sGybT', sourcePortID: 'if_f0rOAt', }, { - sourceNodeID: 'llm_0', + sourceNodeID: 'llm_vTyMa', targetNodeID: 'end_0', }, { - sourceNodeID: 'loop_H8M3U', + sourceNodeID: 'loop_sGybT', targetNodeID: 'end_0', }, ], diff --git a/apps/demo-free-layout/src/nodes/end/form-meta.tsx b/apps/demo-free-layout/src/nodes/end/form-meta.tsx index 0b7c9de4..b8fddf15 100644 --- a/apps/demo-free-layout/src/nodes/end/form-meta.tsx +++ b/apps/demo-free-layout/src/nodes/end/form-meta.tsx @@ -15,7 +15,7 @@ export const renderForm = () => { >) => ( @@ -43,7 +43,7 @@ export const renderForm = () => { )} /> - + ); @@ -52,7 +52,7 @@ export const renderForm = () => { <> - + ); diff --git a/apps/demo-free-layout/src/nodes/llm/index.ts b/apps/demo-free-layout/src/nodes/llm/index.ts index 51829ed6..2b8023b0 100644 --- a/apps/demo-free-layout/src/nodes/llm/index.ts +++ b/apps/demo-free-layout/src/nodes/llm/index.ts @@ -15,7 +15,7 @@ export const LLMNodeRegistry: FlowNodeRegistry = { meta: { size: { width: 360, - height: 305, + height: 300, }, }, onAdd() { @@ -25,10 +25,18 @@ export const LLMNodeRegistry: FlowNodeRegistry = { data: { title: `LLM_${++index}`, inputsValues: { - modelType: { + modelName: { type: 'constant', content: 'gpt-3.5-turbo', }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, temperature: { type: 'constant', content: 0.5, @@ -44,9 +52,15 @@ export const LLMNodeRegistry: FlowNodeRegistry = { }, inputs: { type: 'object', - required: ['modelType', 'temperature', 'prompt'], + required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'], properties: { - modelType: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { type: 'string', }, temperature: { diff --git a/apps/demo-free-layout/src/nodes/loop/index.ts b/apps/demo-free-layout/src/nodes/loop/index.ts index 4e89acfe..fad3936b 100644 --- a/apps/demo-free-layout/src/nodes/loop/index.ts +++ b/apps/demo-free-layout/src/nodes/loop/index.ts @@ -39,8 +39,8 @@ export const LoopNodeRegistry: FlowNodeRegistry = { * 子画布 padding 设置 */ padding: () => ({ - top: 125, - bottom: 100, + top: 120, + bottom: 60, left: 100, right: 100, }), diff --git a/apps/demo-free-layout/src/nodes/loop/loop-form-render.tsx b/apps/demo-free-layout/src/nodes/loop/loop-form-render.tsx index cdc07208..97456965 100644 --- a/apps/demo-free-layout/src/nodes/loop/loop-form-render.tsx +++ b/apps/demo-free-layout/src/nodes/loop/loop-form-render.tsx @@ -14,6 +14,7 @@ interface LoopNodeJSON extends FlowNodeJSON { export const LoopFormRender = ({ form }: FormRenderProps) => { const isSidebar = useIsSidebar(); const { readonly } = useNodeRenderContext(); + const formHeight = 85; const batchFor = ( name={`batchFor`}> @@ -48,7 +49,7 @@ export const LoopFormRender = ({ form }: FormRenderProps) => { {batchFor} - + diff --git a/apps/demo-free-layout/src/plugins/index.ts b/apps/demo-free-layout/src/plugins/index.ts index 0376fd87..83efe551 100644 --- a/apps/demo-free-layout/src/plugins/index.ts +++ b/apps/demo-free-layout/src/plugins/index.ts @@ -1,2 +1,3 @@ export { createSyncVariablePlugin } from './sync-variable-plugin/sync-variable-plugin'; export { createContextMenuPlugin } from './context-menu-plugin'; +export { createRuntimePlugin } from './runtime-plugin'; diff --git a/apps/demo-free-layout/src/plugins/runtime-plugin/browser-client/index.ts b/apps/demo-free-layout/src/plugins/runtime-plugin/browser-client/index.ts new file mode 100644 index 00000000..82c05a4a --- /dev/null +++ b/apps/demo-free-layout/src/plugins/runtime-plugin/browser-client/index.ts @@ -0,0 +1,17 @@ +/* eslint-disable no-console */ +import { TaskCancelAPI, TaskReportAPI, TaskResultAPI, TaskRunAPI } from '@flowgram.ai/runtime-js'; +import { FlowGramAPIName, IRuntimeClient } from '@flowgram.ai/runtime-interface'; +import { injectable } from '@flowgram.ai/free-layout-editor'; + +@injectable() +export class WorkflowRuntimeClient implements IRuntimeClient { + constructor() {} + + public [FlowGramAPIName.TaskRun]: IRuntimeClient[FlowGramAPIName.TaskRun] = TaskRunAPI; + + public [FlowGramAPIName.TaskReport]: IRuntimeClient[FlowGramAPIName.TaskReport] = TaskReportAPI; + + public [FlowGramAPIName.TaskResult]: IRuntimeClient[FlowGramAPIName.TaskResult] = TaskResultAPI; + + public [FlowGramAPIName.TaskCancel]: IRuntimeClient[FlowGramAPIName.TaskCancel] = TaskCancelAPI; +} diff --git a/apps/demo-free-layout/src/plugins/runtime-plugin/create-runtime-plugin.ts b/apps/demo-free-layout/src/plugins/runtime-plugin/create-runtime-plugin.ts new file mode 100644 index 00000000..0826920a --- /dev/null +++ b/apps/demo-free-layout/src/plugins/runtime-plugin/create-runtime-plugin.ts @@ -0,0 +1,23 @@ +import { definePluginCreator, PluginContext } from '@flowgram.ai/free-layout-editor'; + +import { RuntimePluginOptions } from './type'; +import { WorkflowRuntimeServerClient } from './server-client'; +import { WorkflowRuntimeService } from './runtime-service'; +import { WorkflowRuntimeClient } from './browser-client'; + +export const createRuntimePlugin = definePluginCreator({ + onBind({ bind, rebind }, options) { + bind(WorkflowRuntimeClient).toSelf().inSingletonScope(); + bind(WorkflowRuntimeServerClient).toSelf().inSingletonScope(); + if (options.mode === 'server') { + rebind(WorkflowRuntimeClient).to(WorkflowRuntimeServerClient); + } + bind(WorkflowRuntimeService).toSelf().inSingletonScope(); + }, + onInit(ctx, options) { + if (options.mode === 'server') { + const serverClient = ctx.get(WorkflowRuntimeServerClient); + serverClient.init(options.serverConfig); + } + }, +}); diff --git a/apps/demo-free-layout/src/plugins/runtime-plugin/index.ts b/apps/demo-free-layout/src/plugins/runtime-plugin/index.ts new file mode 100644 index 00000000..fbff0a8b --- /dev/null +++ b/apps/demo-free-layout/src/plugins/runtime-plugin/index.ts @@ -0,0 +1,2 @@ +export { createRuntimePlugin } from './create-runtime-plugin'; +export { WorkflowRuntimeClient } from './browser-client'; diff --git a/apps/demo-free-layout/src/plugins/runtime-plugin/runtime-service/index.ts b/apps/demo-free-layout/src/plugins/runtime-plugin/runtime-service/index.ts new file mode 100644 index 00000000..5435cf9c --- /dev/null +++ b/apps/demo-free-layout/src/plugins/runtime-plugin/runtime-service/index.ts @@ -0,0 +1,172 @@ +import { + IReport, + NodeReport, + WorkflowInputs, + WorkflowOutputs, + WorkflowStatus, +} from '@flowgram.ai/runtime-interface'; +import { + injectable, + inject, + WorkflowDocument, + Playground, + WorkflowLineEntity, + WorkflowNodeEntity, + WorkflowNodeLinesData, + Emitter, + getNodeForm, +} from '@flowgram.ai/free-layout-editor'; + +import { WorkflowRuntimeClient } from '../browser-client'; + +const SYNC_TASK_REPORT_INTERVAL = 500; + +interface NodeRunningStatus { + nodeID: string; + status: WorkflowStatus; + nodeResultLength: number; +} + +@injectable() +export class WorkflowRuntimeService { + @inject(Playground) playground: Playground; + + @inject(WorkflowDocument) document: WorkflowDocument; + + @inject(WorkflowRuntimeClient) runtimeClient: WorkflowRuntimeClient; + + private runningNodes: WorkflowNodeEntity[] = []; + + private taskID?: string; + + private syncTaskReportIntervalID?: ReturnType; + + private reportEmitter = new Emitter(); + + private resetEmitter = new Emitter<{}>(); + + public terminatedEmitter = new Emitter<{ + result?: { + inputs: WorkflowInputs; + outputs: WorkflowOutputs; + }; + }>(); + + private nodeRunningStatus: Map; + + public onNodeReportChange = this.reportEmitter.event; + + public onReset = this.resetEmitter.event; + + public onTerminated = this.terminatedEmitter.event; + + public isFlowingLine(line: WorkflowLineEntity) { + return this.runningNodes.some((node) => + node.getData(WorkflowNodeLinesData).inputLines.includes(line) + ); + } + + public async taskRun(inputsString: string): Promise { + if (this.taskID) { + await this.taskCancel(); + } + if (!this.validate()) { + return; + } + this.reset(); + const output = await this.runtimeClient.TaskRun({ + schema: JSON.stringify(this.document.toJSON()), + inputs: JSON.parse(inputsString) as WorkflowInputs, + }); + if (!output) { + this.terminatedEmitter.fire({}); + return; + } + this.taskID = output.taskID; + this.syncTaskReportIntervalID = setInterval(() => { + this.syncTaskReport(); + }, SYNC_TASK_REPORT_INTERVAL); + } + + public async taskCancel(): Promise { + if (!this.taskID) { + return; + } + await this.runtimeClient.TaskCancel({ + taskID: this.taskID, + }); + } + + private async validate(): Promise { + const allForms = this.document.getAllNodes().map((node) => getNodeForm(node)); + const formValidations = await Promise.all(allForms.map(async (form) => form?.validate())); + const validations = formValidations.filter((validation) => validation !== undefined); + const isValid = validations.every((validation) => validation); + return isValid; + } + + private reset(): void { + this.taskID = undefined; + this.nodeRunningStatus = new Map(); + this.runningNodes = []; + if (this.syncTaskReportIntervalID) { + clearInterval(this.syncTaskReportIntervalID); + } + this.resetEmitter.fire({}); + } + + private async syncTaskReport(): Promise { + if (!this.taskID) { + return; + } + const output = await this.runtimeClient.TaskReport({ + taskID: this.taskID, + }); + if (!output) { + clearInterval(this.syncTaskReportIntervalID); + console.error('Sync task report failed'); + return; + } + const { workflowStatus, inputs, outputs } = output; + if (workflowStatus.terminated) { + clearInterval(this.syncTaskReportIntervalID); + if (Object.keys(outputs).length > 0) { + this.terminatedEmitter.fire({ result: { inputs, outputs } }); + } else { + this.terminatedEmitter.fire({}); + } + } + this.updateReport(output); + } + + private updateReport(report: IReport): void { + const { reports } = report; + this.runningNodes = []; + this.document.getAllNodes().forEach((node) => { + const nodeID = node.id; + const nodeReport = reports[nodeID]; + if (!nodeReport) { + return; + } + if (nodeReport.status === WorkflowStatus.Processing) { + this.runningNodes.push(node); + } + const runningStatus = this.nodeRunningStatus.get(nodeID); + if ( + !runningStatus || + nodeReport.status !== runningStatus.status || + nodeReport.snapshots.length !== runningStatus.nodeResultLength + ) { + this.nodeRunningStatus.set(nodeID, { + nodeID, + status: nodeReport.status, + nodeResultLength: nodeReport.snapshots.length, + }); + this.reportEmitter.fire(nodeReport); + this.document.linesManager.forceUpdate(); + } else if (nodeReport.status === WorkflowStatus.Processing) { + this.reportEmitter.fire(nodeReport); + } + }); + } +} diff --git a/apps/demo-free-layout/src/plugins/runtime-plugin/server-client/constant.ts b/apps/demo-free-layout/src/plugins/runtime-plugin/server-client/constant.ts new file mode 100644 index 00000000..a689542e --- /dev/null +++ b/apps/demo-free-layout/src/plugins/runtime-plugin/server-client/constant.ts @@ -0,0 +1,7 @@ +import { ServerConfig } from '../type'; + +export const DEFAULT_SERVER_CONFIG: ServerConfig = { + domain: 'localhost', + port: 4000, + protocol: 'http', +}; diff --git a/apps/demo-free-layout/src/plugins/runtime-plugin/server-client/index.ts b/apps/demo-free-layout/src/plugins/runtime-plugin/server-client/index.ts new file mode 100644 index 00000000..0ce8a93e --- /dev/null +++ b/apps/demo-free-layout/src/plugins/runtime-plugin/server-client/index.ts @@ -0,0 +1,134 @@ +import { + FlowGramAPIName, + IRuntimeClient, + TaskCancelInput, + TaskCancelOutput, + TaskReportInput, + TaskReportOutput, + TaskResultInput, + TaskResultOutput, + TaskRunInput, + TaskRunOutput, +} from '@flowgram.ai/runtime-interface'; +import { injectable } from '@flowgram.ai/free-layout-editor'; + +import type { ServerError } from './type'; +import { DEFAULT_SERVER_CONFIG } from './constant'; +import { ServerConfig } from '../type'; + +@injectable() +export class WorkflowRuntimeServerClient implements IRuntimeClient { + private config: ServerConfig = DEFAULT_SERVER_CONFIG; + + constructor() {} + + public init(config: ServerConfig) { + this.config = config; + } + + public async [FlowGramAPIName.TaskRun](input: TaskRunInput): Promise { + try { + const body = JSON.stringify(input); + const response = await fetch(this.getURL('/api/task/run'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: body, + redirect: 'follow', + }); + const output: TaskRunOutput | ServerError = await response.json(); + if (this.isError(output)) { + console.error('TaskRun failed', output); + return; + } + return output; + } catch (e) { + console.error(e); + return; + } + } + + public async [FlowGramAPIName.TaskReport]( + input: TaskReportInput + ): Promise { + try { + const response = await fetch(this.getURL(`/api/task/report?taskID=${input.taskID}`), { + method: 'GET', + redirect: 'follow', + }); + const output: TaskReportOutput | ServerError = await response.json(); + if (this.isError(output)) { + console.error('TaskReport failed', output); + return; + } + return output; + } catch (e) { + console.error(e); + return; + } + } + + public async [FlowGramAPIName.TaskResult]( + input: TaskResultInput + ): Promise { + try { + const response = await fetch(this.getURL(`/api/task/result?taskID=${input.taskID}`), { + method: 'GET', + redirect: 'follow', + }); + const output: TaskResultOutput | ServerError = await response.json(); + if (this.isError(output)) { + console.error('TaskReport failed', output); + return { + success: false, + }; + } + return output; + } catch (e) { + console.error(e); + return { + success: false, + }; + } + } + + public async [FlowGramAPIName.TaskCancel](input: TaskCancelInput): Promise { + try { + const body = JSON.stringify(input); + const response = await fetch(this.getURL(`/api/task/cancel`), { + method: 'PUT', + redirect: 'follow', + headers: { + 'Content-Type': 'application/json', + }, + body, + }); + const output: TaskCancelOutput | ServerError = await response.json(); + if (this.isError(output)) { + console.error('TaskReport failed', output); + return { + success: false, + }; + } + return output; + } catch (e) { + console.error(e); + return { + success: false, + }; + } + } + + private isError(output: unknown | undefined): output is ServerError { + return !!output && (output as ServerError).code !== undefined; + } + + private getURL(path: string): string { + const protocol = this.config.protocol ?? window.location.protocol; + const host = this.config.port + ? `${this.config.domain}:${this.config.port}` + : this.config.domain; + return `${protocol}://${host}${path}`; + } +} diff --git a/apps/demo-free-layout/src/plugins/runtime-plugin/server-client/type.ts b/apps/demo-free-layout/src/plugins/runtime-plugin/server-client/type.ts new file mode 100644 index 00000000..d0c90a22 --- /dev/null +++ b/apps/demo-free-layout/src/plugins/runtime-plugin/server-client/type.ts @@ -0,0 +1,4 @@ +export interface ServerError { + code: string; + message: string; +} diff --git a/apps/demo-free-layout/src/plugins/runtime-plugin/type.ts b/apps/demo-free-layout/src/plugins/runtime-plugin/type.ts new file mode 100644 index 00000000..0f76f17b --- /dev/null +++ b/apps/demo-free-layout/src/plugins/runtime-plugin/type.ts @@ -0,0 +1,16 @@ +export interface RuntimeBrowserOptions { + mode?: 'browser'; +} + +export interface RuntimeServerOptions { + mode: 'server'; + serverConfig: ServerConfig; +} + +export type RuntimePluginOptions = RuntimeBrowserOptions | RuntimeServerOptions; + +export interface ServerConfig { + domain: string; + port?: number; + protocol?: string; +} diff --git a/apps/demo-free-layout/src/services/index.ts b/apps/demo-free-layout/src/services/index.ts index 21259a88..01db6d60 100644 --- a/apps/demo-free-layout/src/services/index.ts +++ b/apps/demo-free-layout/src/services/index.ts @@ -1,2 +1 @@ export { CustomService } from './custom-service'; -export { RunningService } from './running-service'; diff --git a/apps/demo-free-layout/src/services/running-service.ts b/apps/demo-free-layout/src/services/running-service.ts deleted file mode 100644 index f7d2e9a2..00000000 --- a/apps/demo-free-layout/src/services/running-service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - injectable, - inject, - WorkflowDocument, - Playground, - delay, - WorkflowLineEntity, - WorkflowNodeEntity, - WorkflowNodeLinesData, -} from '@flowgram.ai/free-layout-editor'; -const RUNNING_INTERVAL = 1000; - -@injectable() -export class RunningService { - @inject(Playground) playground: Playground; - - @inject(WorkflowDocument) document: WorkflowDocument; - - private _runningNodes: WorkflowNodeEntity[] = []; - - async addRunningNode(node: WorkflowNodeEntity): Promise { - this._runningNodes.push(node); - node.renderData.node.classList.add('node-running'); - this.document.linesManager.forceUpdate(); // Refresh line renderer - await delay(RUNNING_INTERVAL); - // Child Nodes - await Promise.all(node.blocks.map((nextNode) => this.addRunningNode(nextNode))); - // Sibling Nodes - const nextNodes = node.getData(WorkflowNodeLinesData).outputNodes; - await Promise.all(nextNodes.map((nextNode) => this.addRunningNode(nextNode))); - } - - async startRun(): Promise { - await this.addRunningNode(this.document.getNode('start_0')!); - this._runningNodes.forEach((node) => { - node.renderData.node.classList.remove('node-running'); - }); - this._runningNodes = []; - this.document.linesManager.forceUpdate(); - } - - isFlowingLine(line: WorkflowLineEntity) { - return this._runningNodes.some((node) => - node.getData(WorkflowNodeLinesData).outputLines.includes(line) - ); - } -} diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index a871ca0f..935af3b3 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -223,6 +223,12 @@ importers: '@flowgram.ai/minimap-plugin': specifier: workspace:* version: link:../../packages/plugins/minimap-plugin + '@flowgram.ai/runtime-interface': + specifier: workspace:* + version: link:../../packages/runtime/interface + '@flowgram.ai/runtime-js': + specifier: workspace:* + version: link:../../packages/runtime/js-core lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -764,7 +770,7 @@ importers: version: 15.13.0 openai: specifier: ~4.98.0 - version: 4.98.0 + version: 4.98.0(ws@8.18.0)(zod@3.25.56) raw-loader: specifier: ^4.0.2 version: 4.0.2(webpack@5.76.0) @@ -1755,7 +1761,7 @@ importers: version: 8.57.1 tsup: specifier: ^8.0.1 - version: 8.3.5(typescript@5.0.4) + version: 8.3.5(tsx@4.19.4)(typescript@5.0.4) vitest: specifier: ^0.34.6 version: 0.34.6(jsdom@22.1.0) @@ -2472,7 +2478,7 @@ importers: version: 5.3.11(@babel/core@7.26.10)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1) tsup: specifier: ^8.0.1 - version: 8.3.5(typescript@5.0.4) + version: 8.3.5(tsx@4.19.4)(typescript@5.0.4) vitest: specifier: ^0.34.6 version: 0.34.6(jsdom@22.1.0) @@ -3260,7 +3266,7 @@ importers: version: 5.3.11(@babel/core@7.26.10)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1) tsup: specifier: ^8.0.1 - version: 8.3.5(typescript@5.0.4) + version: 8.3.5(tsx@4.19.4)(typescript@5.0.4) vitest: specifier: ^0.34.6 version: 0.34.6(jsdom@22.1.0) @@ -3735,6 +3741,192 @@ importers: specifier: ^0.34.6 version: 0.34.6(jsdom@22.1.0) + ../../packages/runtime/interface: + dependencies: + zod: + specifier: ^3.24.4 + version: 3.25.56 + devDependencies: + '@flowgram.ai/eslint-config': + specifier: workspace:* + version: link:../../../config/eslint-config + '@flowgram.ai/ts-config': + specifier: workspace:* + version: link:../../../config/ts-config + eslint: + specifier: ^8.54.0 + version: 8.57.1 + tsup: + specifier: ^8.0.1 + version: 8.3.5(typescript@5.0.4) + typescript: + specifier: ^5.0.4 + version: 5.0.4 + vitest: + specifier: ^0.34.6 + version: 0.34.6(jsdom@22.1.0) + + ../../packages/runtime/js-core: + dependencies: + '@langchain/core': + specifier: ^0.3.57 + version: 0.3.57 + '@langchain/openai': + specifier: ^0.5.11 + version: 0.5.12(@langchain/core@0.3.57)(ws@8.18.0) + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + uuid: + specifier: ^9.0.0 + version: 9.0.1 + zod: + specifier: ^3.24.4 + version: 3.25.56 + devDependencies: + '@flowgram.ai/eslint-config': + specifier: workspace:* + version: link:../../../config/eslint-config + '@flowgram.ai/runtime-interface': + specifier: workspace:* + version: link:../interface + '@flowgram.ai/ts-config': + specifier: workspace:* + version: link:../../../config/ts-config + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/uuid': + specifier: ^9.0.1 + version: 9.0.8 + '@vitest/coverage-v8': + specifier: ^0.32.0 + version: 0.32.4(vitest@0.34.6) + dotenv: + specifier: ~16.5.0 + version: 16.5.0 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + tsup: + specifier: ^8.0.1 + version: 8.3.5(typescript@5.0.4) + typescript: + specifier: ^5.0.4 + version: 5.0.4 + vitest: + specifier: ^0.34.6 + version: 0.34.6(jsdom@22.1.0) + + ../../packages/runtime/nodejs: + dependencies: + '@fastify/cors': + specifier: ^8.2.1 + version: 8.5.0 + '@fastify/swagger': + specifier: ^8.5.1 + version: 8.15.0 + '@fastify/swagger-ui': + specifier: 4.1.0 + version: 4.1.0 + '@fastify/websocket': + specifier: ^10.0.1 + version: 10.0.1 + '@flowgram.ai/runtime-interface': + specifier: workspace:* + version: link:../interface + '@flowgram.ai/runtime-js': + specifier: workspace:* + version: link:../js-core + '@langchain/core': + specifier: ^0.3.57 + version: 0.3.57 + '@langchain/openai': + specifier: ^0.5.11 + version: 0.5.12(@langchain/core@0.3.57)(ws@8.18.0) + '@trpc/server': + specifier: ^10.27.1 + version: 10.45.2 + fastify: + specifier: ^4.17.0 + version: 4.29.1 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + trpc-openapi: + specifier: ^1.2.0 + version: 1.2.0(@trpc/server@10.45.2)(@types/node@18.19.68)(zod@3.25.56) + tslib: + specifier: ^2.8.1 + version: 2.8.1 + ws: + specifier: ^8.0.0 + version: 8.18.0 + zod: + specifier: ^3.24.4 + version: 3.25.56 + devDependencies: + '@babel/eslint-parser': + specifier: ~7.19.1 + version: 7.19.1(@babel/core@7.26.10)(eslint@8.57.1) + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.1 + '@flowgram.ai/eslint-config': + specifier: workspace:* + version: link:../../../config/eslint-config + '@flowgram.ai/ts-config': + specifier: workspace:* + version: link:../../../config/ts-config + '@types/cors': + specifier: ^2.8.13 + version: 2.8.19 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/node': + specifier: ^18 + version: 18.19.68 + '@types/ws': + specifier: ^8.2.0 + version: 8.18.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.10.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.0.4) + '@typescript-eslint/parser': + specifier: ^6.10.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.0.4) + '@vitest/coverage-v8': + specifier: ^0.32.0 + version: 0.32.4(vitest@0.34.6) + dotenv: + specifier: ~16.5.0 + version: 16.5.0 + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-plugin-json: + specifier: ^4.0.1 + version: 4.0.1 + npm-run-all: + specifier: ^4.1.5 + version: 4.1.5 + tsup: + specifier: ^8.0.1 + version: 8.3.5(tsx@4.19.4)(typescript@5.0.4) + tsx: + specifier: ~4.19.4 + version: 4.19.4 + typescript: + specifier: ^5.0.4 + version: 5.0.4 + vitest: + specifier: ^0.34.6 + version: 0.34.6(jsdom@22.1.0) + wait-port: + specifier: ^1.0.1 + version: 1.1.0 + ../../packages/variable-engine/variable-core: dependencies: '@flowgram.ai/core': @@ -5230,6 +5422,10 @@ packages: /@bufbuild/protobuf@2.2.3: resolution: {integrity: sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==} + /@cfworker/json-schema@4.1.1: + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + dev: false + /@codemirror/autocomplete@6.18.4: resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==} dependencies: @@ -6236,6 +6432,100 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: true + /@fastify/accept-negotiator@1.1.0: + resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} + engines: {node: '>=14'} + dev: false + + /@fastify/ajv-compiler@3.6.0: + resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} + dependencies: + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + fast-uri: 2.4.0 + dev: false + + /@fastify/cors@8.5.0: + resolution: {integrity: sha512-/oZ1QSb02XjP0IK1U0IXktEsw/dUBTxJOW7IpIeO8c/tNalw/KjoNSJv1Sf6eqoBPO+TDGkifq6ynFK3v68HFQ==} + dependencies: + fastify-plugin: 4.5.1 + mnemonist: 0.39.6 + dev: false + + /@fastify/error@3.4.1: + resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} + dev: false + + /@fastify/fast-json-stringify-compiler@4.3.0: + resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + dependencies: + fast-json-stringify: 5.16.1 + dev: false + + /@fastify/merge-json-schemas@0.1.1: + resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} + dependencies: + fast-deep-equal: 3.1.3 + dev: false + + /@fastify/send@2.1.0: + resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==} + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.0 + mime: 3.0.0 + dev: false + + /@fastify/static@7.0.4: + resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==} + dependencies: + '@fastify/accept-negotiator': 1.1.0 + '@fastify/send': 2.1.0 + content-disposition: 0.5.4 + fastify-plugin: 4.5.1 + fastq: 1.17.1 + glob: 10.4.5 + dev: false + + /@fastify/swagger-ui@4.1.0: + resolution: {integrity: sha512-Bqsd6VFQR7WoT6eRammOF8/gXf5GKywq2zYy8/3fj2rsZw43cmXdfsEKxVAmAwOW2Nv+dnyQaf5qM6kBqyXRlw==} + dependencies: + '@fastify/static': 7.0.4 + fastify-plugin: 4.5.1 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.8.0 + dev: false + + /@fastify/swagger@8.15.0: + resolution: {integrity: sha512-zy+HEEKFqPMS2sFUsQU5X0MHplhKJvWeohBwTCkBAJA/GDYGLGUWQaETEhptiqxK7Hs0fQB9B4MDb3pbwIiCwA==} + dependencies: + fastify-plugin: 4.5.1 + json-schema-resolver: 2.0.0 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.8.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@fastify/websocket@10.0.1: + resolution: {integrity: sha512-8/pQIxTPRD8U94aILTeJ+2O3el/r19+Ej5z1O1mXlqplsUH7KzCjAI0sgd5DM/NoPjAi5qLFNIjgM5+9/rGSNw==} + dependencies: + duplexify: 4.1.3 + fastify-plugin: 4.5.1 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /@hapi/bourne@3.0.0: + resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} + dev: false + /@humanwhocodes/config-array@0.13.0: resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -6510,6 +6800,42 @@ packages: '@jridgewell/sourcemap-codec': 1.5.0 dev: false + /@langchain/core@0.3.57: + resolution: {integrity: sha512-jz28qCTKJmi47b6jqhQ6vYRTG5jRpqhtPQjriRTB5wR8mgvzo6xKs0fG/kExS3ZvM79ytD1npBvgf8i19xOo9Q==} + engines: {node: '>=18'} + dependencies: + '@cfworker/json-schema': 4.1.1 + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.20 + langsmith: 0.3.30 + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 10.0.0 + zod: 3.25.56 + zod-to-json-schema: 3.24.5(zod@3.25.56) + transitivePeerDependencies: + - openai + dev: false + + /@langchain/openai@0.5.12(@langchain/core@0.3.57)(ws@8.18.0): + resolution: {integrity: sha512-k7rxBY3ed/HIiMLd6HBqFibsfB0+L6c82H8JgXDqKeyUoACJIi1JaKHXmofmCeF2SBXBU9dog4gEGpHfcUDGUA==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': '>=0.3.48 <0.4.0' + dependencies: + '@langchain/core': 0.3.57 + js-tiktoken: 1.0.20 + openai: 4.98.0(ws@8.18.0)(zod@3.25.56) + zod: 3.25.56 + zod-to-json-schema: 3.24.5(zod@3.25.56) + transitivePeerDependencies: + - encoding + - ws + dev: false + /@lezer/common@1.2.3: resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} dev: false @@ -6550,6 +6876,11 @@ packages: '@lezer/common': 1.2.3 dev: false + /@lukeed/ms@2.0.2: + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + dev: false + /@marijn/find-cluster-break@1.0.2: resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} dev: false @@ -8042,6 +8373,10 @@ packages: engines: {node: '>= 10'} dev: true + /@trpc/server@10.45.2: + resolution: {integrity: sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg==} + dev: false + /@tsconfig/node10@1.0.11: resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} dev: false @@ -8114,6 +8449,12 @@ packages: resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} dev: true + /@types/cors@2.8.19: + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + dependencies: + '@types/node': 18.19.68 + dev: true + /@types/debug@4.1.12: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} dependencies: @@ -8263,7 +8604,6 @@ packages: dependencies: '@types/node': 18.19.68 form-data: 4.0.1 - dev: true /@types/node@18.19.68: resolution: {integrity: sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==} @@ -8311,6 +8651,10 @@ packages: '@types/prop-types': 15.7.14 csstype: 3.1.3 + /@types/retry@0.12.0: + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + dev: false + /@types/scheduler@0.16.8: resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} dev: true @@ -8342,6 +8686,20 @@ packages: /@types/unist@3.0.3: resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + /@types/uuid@10.0.0: + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + dev: false + + /@types/uuid@9.0.8: + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + dev: true + + /@types/ws@8.18.1: + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + dependencies: + '@types/node': 18.19.68 + dev: true + /@typescript-eslint/eslint-plugin@5.38.1(@typescript-eslint/parser@5.38.1)(eslint@8.57.1)(typescript@5.0.4): resolution: {integrity: sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -8906,7 +9264,18 @@ packages: engines: {node: '>=6.5'} dependencies: event-target-shim: 5.0.1 - dev: true + + /abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + dev: false + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false /acorn-import-assertions@1.9.0(acorn@8.14.0): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} @@ -8948,7 +9317,6 @@ packages: engines: {node: '>= 8.0.0'} dependencies: humanize-ms: 1.2.1 - dev: true /ajv-formats@2.1.1(ajv@8.17.1): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} @@ -8960,6 +9328,17 @@ packages: dependencies: ajv: 8.17.1 + /ajv-formats@3.0.1(ajv@8.17.1): + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.17.1 + dev: false + /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -9013,6 +9392,13 @@ packages: /ansi-sequence-parser@1.1.1: resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -9022,7 +9408,6 @@ packages: /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - dev: true /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} @@ -9170,19 +9555,30 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true /at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} dev: false + /atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + dev: false + /available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} dependencies: possible-typed-array-names: 1.0.0 + /avvio@8.4.0: + resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==} + dependencies: + '@fastify/error': 3.4.1 + fastq: 1.17.1 + dev: false + /axe-core@4.10.2: resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} engines: {node: '>=4'} @@ -9367,6 +9763,11 @@ packages: dependencies: streamsearch: 1.1.0 + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -9410,6 +9811,11 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: false + /camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} @@ -9435,6 +9841,15 @@ packages: type-detect: 4.1.0 dev: true + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -9569,16 +9984,37 @@ packages: engines: {node: '>=6'} dev: false + /co-body@6.2.0: + resolution: {integrity: sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==} + engines: {node: '>=8.0.0'} + dependencies: + '@hapi/bourne': 3.0.0 + inflation: 2.1.0 + qs: 6.14.0 + raw-body: 2.5.2 + type-is: 1.6.18 + dev: false + /collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} dev: false + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -9607,7 +10043,6 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: true /comma-separated-tokens@1.0.8: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} @@ -9628,6 +10063,11 @@ packages: engines: {node: '>= 6'} dev: true + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + dev: true + /compute-scroll-into-view@1.0.20: resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} dev: false @@ -9644,6 +10084,12 @@ packages: engines: {node: ^14.18.0 || >=16.10.0} dev: true + /console-table-printer@2.14.2: + resolution: {integrity: sha512-TyXKHIzSBFAuxRpgB4MA3RhFVzghJGpG8/eHmpWGm/2ezdswpbdVkxN7xTvDM3snIDKc8UrUs2NiR4LFjv/F1w==} + dependencies: + simple-wcswidth: 1.0.1 + dev: false + /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -9654,6 +10100,15 @@ packages: /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + /cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + dev: false + + /cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + dev: false + /copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} dependencies: @@ -9724,6 +10179,12 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + dependencies: + uncrypto: 0.1.3 + dev: false + /css-color-keywords@1.0.0: resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} engines: {node: '>=4'} @@ -9825,6 +10286,11 @@ packages: ms: 2.1.3 supports-color: 5.5.0 + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: false + /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} dev: true @@ -9959,15 +10425,32 @@ packages: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + /defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dev: true + + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + /destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + dev: false + /detect-indent@7.0.1: resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==} engines: {node: '>=12.20'} @@ -10123,6 +10606,15 @@ packages: resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} dev: false + /duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -10174,6 +10666,12 @@ packages: dev: true optional: true + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + /error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} dependencies: @@ -10460,10 +10958,13 @@ packages: resolution: {integrity: sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==} dev: false + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - dev: false /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} @@ -10950,7 +11451,10 @@ packages: /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - dev: true + + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} @@ -11027,10 +11531,18 @@ packages: engines: {node: '>=18'} dev: true + /fast-content-type-parse@1.1.0: + resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} + dev: false + /fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} dev: false + /fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + dev: false + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -11066,12 +11578,64 @@ packages: /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + /fast-json-stringify@5.16.1: + resolution: {integrity: sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==} + dependencies: + '@fastify/merge-json-schemas': 0.1.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-deep-equal: 3.1.3 + fast-uri: 2.4.0 + json-schema-ref-resolver: 1.0.1 + rfdc: 1.4.1 + dev: false + /fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + /fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + dependencies: + fast-decode-uri-component: 1.0.1 + dev: false + + /fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + dev: false + + /fast-uri@2.4.0: + resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==} + dev: false + /fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + /fastify-plugin@4.5.1: + resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + dev: false + + /fastify@4.29.1: + resolution: {integrity: sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==} + dependencies: + '@fastify/ajv-compiler': 3.6.0 + '@fastify/error': 3.4.1 + '@fastify/fast-json-stringify-compiler': 4.3.0 + abstract-logging: 2.0.1 + avvio: 8.4.0 + fast-content-type-parse: 1.1.0 + fast-json-stringify: 5.16.1 + find-my-way: 8.2.2 + light-my-request: 5.14.0 + pino: 9.7.0 + process-warning: 3.0.0 + proxy-addr: 2.0.7 + rfdc: 1.4.1 + secure-json-parse: 2.7.0 + semver: 7.6.3 + toad-cache: 3.7.0 + dev: false + /fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} dependencies: @@ -11167,6 +11731,15 @@ packages: dependencies: to-regex-range: 5.0.1 + /find-my-way@8.2.2: + resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==} + engines: {node: '>=14'} + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 3.1.0 + dev: false + /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -11207,7 +11780,6 @@ packages: /form-data-encoder@1.7.2: resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} - dev: true /form-data@2.5.3: resolution: {integrity: sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==} @@ -11227,7 +11799,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: true /format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} @@ -11239,7 +11810,6 @@ packages: dependencies: node-domexception: 1.0.0 web-streams-polyfill: 4.0.0-beta.3 - dev: true /formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} @@ -11247,6 +11817,16 @@ packages: dependencies: fetch-blob: 3.2.0 + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + /from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} dependencies: @@ -11497,6 +12077,20 @@ packages: section-matter: 1.0.0 strip-bom-string: 1.0.0 + /h3@1.15.3: + resolution: {integrity: sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==} + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.0 + radix3: 1.1.2 + ufo: 1.6.1 + uncrypto: 0.1.3 + dev: false + /handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -11701,6 +12295,10 @@ packages: dependencies: react-is: 16.13.1 + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + /html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -11754,6 +12352,17 @@ packages: resolution: {integrity: sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==} dev: false + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + /http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -11783,7 +12392,6 @@ packages: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} dependencies: ms: 2.1.3 - dev: true /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} @@ -11833,6 +12441,11 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + /inflation@2.1.0: + resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==} + engines: {node: '>= 0.8.0'} + dev: false + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -11908,6 +12521,15 @@ packages: reflect-metadata: 0.2.2 dev: false + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + dev: false + /is-absolute-url@4.0.1: resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -11945,6 +12567,10 @@ packages: call-bind: 1.0.8 get-intrinsic: 1.2.6 + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} requiresBuild: true @@ -12291,6 +12917,12 @@ packages: engines: {node: '>=10'} dev: true + /js-tiktoken@1.0.20: + resolution: {integrity: sha512-Xlaqhhs8VfCd6Sh7a1cFkZHQbYTLCwVJJWiHVxBYzLPxW0XsoxBy1hitmjkdIjD3Aon5BXLHFwU5O8WUx6HH+A==} + dependencies: + base64-js: 1.5.1 + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -12363,9 +12995,30 @@ packages: /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + /json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true + /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + /json-schema-ref-resolver@1.0.1: + resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + dependencies: + fast-deep-equal: 3.1.3 + dev: false + + /json-schema-resolver@2.0.0: + resolution: {integrity: sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==} + engines: {node: '>=10'} + dependencies: + debug: 4.4.0(supports-color@5.5.0) + rfdc: 1.4.1 + uri-js: 4.4.1 + transitivePeerDependencies: + - supports-color + dev: false + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -12424,6 +13077,23 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + /langsmith@0.3.30: + resolution: {integrity: sha512-ZaiaOx9MysuSQlAkRw8mjm7iqhrlF7HI0LCTLxiNBEWBPywdkgI7UnN+s7KtlRiM0tP1cOLm+dQY++Fi33jkPQ==} + peerDependencies: + openai: '*' + peerDependenciesMeta: + openai: + optional: true + dependencies: + '@types/uuid': 10.0.0 + chalk: 4.1.2 + console-table-printer: 2.14.2 + p-queue: 6.6.2 + p-retry: 4.6.2 + semver: 7.6.3 + uuid: 10.0.0 + dev: false + /language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -12509,6 +13179,14 @@ packages: prelude-ls: 1.2.1 type-check: 0.4.0 + /light-my-request@5.14.0: + resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} + dependencies: + cookie: 0.7.2 + process-warning: 3.0.0 + set-cookie-parser: 2.7.1 + dev: false + /lightningcss-darwin-arm64@1.29.2: resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} engines: {node: '>= 12.0.0'} @@ -12626,6 +13304,16 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true + /load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + dev: true + /load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12657,6 +13345,10 @@ packages: /lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: false + /lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} dev: false @@ -13146,6 +13838,11 @@ packages: '@types/mdast': 4.0.4 dev: false + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + /medium-zoom@1.1.0: resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==} @@ -13153,6 +13850,15 @@ packages: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} dev: false + /memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + dev: true + + /merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -13160,6 +13866,11 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + /micromark-core-commonmark@1.1.0: resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} dependencies: @@ -13817,8 +14528,12 @@ packages: engines: {node: '>=4'} hasBin: true requiresBuild: true - dev: true - optional: true + + /mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: false /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -13876,6 +14591,12 @@ packages: ufo: 1.5.4 dev: true + /mnemonist@0.39.6: + resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==} + dependencies: + obliterator: 2.0.5 + dev: false + /monaco-editor@0.52.2: resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} dev: false @@ -13887,6 +14608,11 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + dev: false + /mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -13931,6 +14657,11 @@ packages: dev: true optional: true + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -14000,7 +14731,6 @@ packages: optional: true dependencies: whatwg-url: 5.0.0 - dev: true /node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} @@ -14010,9 +14740,47 @@ packages: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + /node-mock-http@1.0.0: + resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} + dev: false + + /node-mocks-http@1.17.2(@types/node@18.19.68): + resolution: {integrity: sha512-HVxSnjNzE9NzoWMx9T9z4MLqwMpLwVvA0oVZ+L+gXskYXEJ6tFn3Kx4LargoB6ie7ZlCLplv7QbWO6N+MysWGA==} + engines: {node: '>=14'} + peerDependencies: + '@types/express': ^4.17.21 || ^5.0.0 + '@types/node': '*' + peerDependenciesMeta: + '@types/express': + optional: true + '@types/node': + optional: true + dependencies: + '@types/node': 18.19.68 + accepts: 1.3.8 + content-disposition: 0.5.4 + depd: 1.1.2 + fresh: 0.5.2 + merge-descriptors: 1.0.3 + methods: 1.1.2 + mime: 1.6.0 + parseurl: 1.3.3 + range-parser: 1.2.1 + type-is: 1.6.18 + dev: false + /node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + /normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.8 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + dev: true + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -14026,6 +14794,22 @@ packages: sort-keys: 2.0.0 dev: false + /npm-run-all@4.1.5: + resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} + engines: {node: '>= 4'} + hasBin: true + dependencies: + ansi-styles: 3.2.1 + chalk: 2.4.2 + cross-spawn: 6.0.6 + memorystream: 0.3.1 + minimatch: 3.1.2 + pidtree: 0.3.1 + read-pkg: 3.0.0 + shell-quote: 1.8.3 + string.prototype.padend: 3.1.6 + dev: true + /npm-run-path@2.0.2: resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} engines: {node: '>=4'} @@ -14117,6 +14901,15 @@ packages: define-properties: 1.2.1 es-object-atoms: 1.0.0 + /obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + dev: false + + /on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -14128,7 +14921,7 @@ packages: dependencies: mimic-fn: 2.1.0 - /openai@4.98.0: + /openai@4.98.0(ws@8.18.0)(zod@3.25.56): resolution: {integrity: sha512-TmDKur1WjxxMPQAtLG5sgBSCJmX7ynTsGmewKzoDwl1fRxtbLOsiR0FA/AOAAtYUmP6azal+MYQuOENfdU+7yg==} hasBin: true peerDependencies: @@ -14147,9 +14940,14 @@ packages: form-data-encoder: 1.7.2 formdata-node: 4.4.1 node-fetch: 2.7.0 + ws: 8.18.0 + zod: 3.25.56 transitivePeerDependencies: - encoding - dev: true + + /openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + dev: false /optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -14226,6 +15024,22 @@ packages: dependencies: p-limit: 3.1.0 + /p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + dev: false + + /p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + dev: false + /p-timeout@2.0.1: resolution: {integrity: sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==} engines: {node: '>=4'} @@ -14233,6 +15047,13 @@ packages: p-finally: 1.0.0 dev: false + /p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + dependencies: + p-finally: 1.0.0 + dev: false + /package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -14264,6 +15085,14 @@ packages: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + /parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: true + /parse-node-version@1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} @@ -14280,6 +15109,11 @@ packages: leac: 0.6.0 peberminta: 0.9.0 + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -14307,6 +15141,13 @@ packages: lru-cache: 10.4.3 minipass: 7.1.2 + /path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + dependencies: + pify: 3.0.0 + dev: true + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -14344,6 +15185,12 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + /pidtree@0.3.1: + resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} + engines: {node: '>=0.10'} + hasBin: true + dev: true + /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -14352,7 +15199,6 @@ packages: /pify@3.0.0: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} - dev: false /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} @@ -14371,6 +15217,33 @@ packages: engines: {node: '>=0.10.0'} dev: false + /pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + dependencies: + split2: 4.2.0 + dev: false + + /pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + dev: false + + /pino@9.7.0: + resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + dev: false + /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -14404,7 +15277,7 @@ packages: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} - /postcss-load-config@6.0.1: + /postcss-load-config@6.0.1(tsx@4.19.4): resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} peerDependencies: @@ -14423,6 +15296,7 @@ packages: optional: true dependencies: lilconfig: 3.1.3 + tsx: 4.19.4 dev: true /postcss-value-parser@4.2.0: @@ -14518,6 +15392,14 @@ packages: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: false + /process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + dev: false + + /process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + dev: false + /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: @@ -14533,6 +15415,14 @@ packages: /property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + /prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} requiresBuild: true @@ -14555,6 +15445,13 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + /qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.1.0 + dev: false + /query-string@5.1.1: resolution: {integrity: sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==} engines: {node: '>=0.10.0'} @@ -14571,11 +15468,34 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + /quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + dev: false + + /radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + dev: false + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: safe-buffer: 5.2.1 + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + /raw-loader@4.0.2(webpack@5.76.0): resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==} engines: {node: '>= 10.13.0'} @@ -14756,6 +15676,15 @@ packages: dependencies: loose-envify: 1.4.0 + /read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + dev: true + /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: @@ -14788,6 +15717,11 @@ packages: engines: {node: '>= 14.16.0'} dev: true + /real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + dev: false + /rechoir@0.6.2: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} @@ -15096,10 +16030,24 @@ packages: signal-exit: 3.0.7 dev: false + /ret@0.4.3: + resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==} + engines: {node: '>=10'} + dev: false + + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + /rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + dev: false + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -15250,6 +16198,17 @@ packages: es-errors: 1.3.0 is-regex: 1.2.1 + /safe-regex2@3.1.0: + resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} + dependencies: + ret: 0.4.3 + dev: false + + /safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + dev: false + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -15511,6 +16470,10 @@ packages: extend-shallow: 2.0.1 kind-of: 6.0.3 + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: false + /seek-bzip@1.0.6: resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==} hasBin: true @@ -15546,6 +16509,10 @@ packages: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} dev: false + /set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + dev: false + /set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -15566,6 +16533,10 @@ packages: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + /shallow-clone@3.0.1: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} @@ -15628,6 +16599,11 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + /shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + dev: true + /shelljs@0.9.2: resolution: {integrity: sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==} engines: {node: '>=18'} @@ -15710,10 +16686,20 @@ packages: is-arrayish: 0.3.2 optional: true + /simple-wcswidth@1.0.1: + resolution: {integrity: sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==} + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + /sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + dependencies: + atomic-sleep: 1.0.0 + dev: false + /sort-keys-length@1.0.1: resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} engines: {node: '>=0.10.0'} @@ -15784,6 +16770,33 @@ packages: /space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + /spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.21 + dev: true + + /spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 + dev: true + + /spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + dev: true + + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + dev: false + /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -15810,6 +16823,11 @@ packages: outvariant: 1.4.0 dev: false + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + /std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} dev: true @@ -15821,6 +16839,10 @@ packages: internal-slot: 1.0.7 dev: true + /stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + dev: false + /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -15875,6 +16897,16 @@ packages: set-function-name: 2.0.2 side-channel: 1.1.0 + /string.prototype.padend@3.1.6: + resolution: {integrity: sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + dev: true + /string.prototype.repeat@1.0.0: resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} dependencies: @@ -16185,6 +17217,12 @@ packages: any-promise: 1.3.0 dev: true + /thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + dependencies: + real-require: 0.2.0 + dev: false + /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: false @@ -16244,9 +17282,19 @@ packages: dependencies: is-number: 7.0.0 + /toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + dev: false + /toggle-selection@1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + /tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -16259,7 +17307,6 @@ packages: /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: true /tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -16292,6 +17339,25 @@ packages: /trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + /trpc-openapi@1.2.0(@trpc/server@10.45.2)(@types/node@18.19.68)(zod@3.25.56): + resolution: {integrity: sha512-pfYoCd/3KYXWXvUPZBKJw455OOwngKN/6SIcj7Yit19OMLJ+8yVZkEvGEeg5wUSwfsiTdRsKuvqkRPXVSwV7ew==} + peerDependencies: + '@trpc/server': ^10.0.0 + zod: ^3.14.4 + dependencies: + '@trpc/server': 10.45.2 + co-body: 6.2.0 + h3: 1.15.3 + lodash.clonedeep: 4.5.0 + node-mocks-http: 1.17.2(@types/node@18.19.68) + openapi-types: 12.1.3 + zod: 3.25.56 + zod-to-json-schema: 3.24.5(zod@3.25.56) + transitivePeerDependencies: + - '@types/express' + - '@types/node' + dev: false + /ts-api-utils@1.4.3(typescript@5.0.4): resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -16349,6 +17415,49 @@ packages: /tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + /tsup@8.3.5(tsx@4.19.4)(typescript@5.0.4): + resolution: {integrity: sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 5.0.0(esbuild@0.24.0) + cac: 6.7.14 + chokidar: 4.0.1 + consola: 3.2.3 + debug: 4.4.0(supports-color@5.5.0) + esbuild: 0.24.0 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(tsx@4.19.4) + resolve-from: 5.0.0 + rollup: 4.40.2 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.1 + tinyglobby: 0.2.13 + tree-kill: 1.2.2 + typescript: 5.0.4 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + dev: true + /tsup@8.3.5(typescript@5.0.4): resolution: {integrity: sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==} engines: {node: '>=18'} @@ -16376,7 +17485,7 @@ packages: esbuild: 0.24.0 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1 + postcss-load-config: 6.0.1(tsx@4.19.4) resolve-from: 5.0.0 rollup: 4.28.1 source-map: 0.8.0-beta.0 @@ -16433,6 +17542,14 @@ packages: engines: {node: '>=10'} dev: false + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + /type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} dev: false @@ -16524,6 +17641,10 @@ packages: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} dev: true + /ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + dev: false + /uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -16546,6 +17667,10 @@ packages: through: 2.3.8 dev: false + /uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + dev: false + /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -16686,6 +17811,11 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + /update-browserslist-db@1.1.1(browserslist@4.24.2): resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -16729,6 +17859,16 @@ packages: engines: {node: '>= 4'} dev: false + /uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + dev: false + + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + /uvu@0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} engines: {node: '>=8'} @@ -16752,6 +17892,13 @@ packages: convert-source-map: 2.0.0 dev: true + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + dev: true + /varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} @@ -17006,6 +18153,18 @@ packages: xml-name-validator: 4.0.0 dev: true + /wait-port@1.1.0: + resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chalk: 4.1.2 + commander: 9.5.0 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + /watchpack@2.4.2: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} @@ -17029,11 +18188,9 @@ packages: /web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} - dev: true /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: true /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -17121,7 +18278,6 @@ packages: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - dev: true /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -17251,7 +18407,6 @@ packages: optional: true utf-8-validate: optional: true - dev: true /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} @@ -17274,6 +18429,12 @@ packages: engines: {node: '>=18'} dev: false + /yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} + hasBin: true + dev: false + /yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} dependencies: @@ -17300,5 +18461,16 @@ packages: engines: {node: '>=18'} dev: false + /zod-to-json-schema@3.24.5(zod@3.25.56): + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + peerDependencies: + zod: ^3.24.1 + dependencies: + zod: 3.25.56 + dev: false + + /zod@3.25.56: + resolution: {integrity: sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==} + /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} diff --git a/cspell.json b/cspell.json index 0b6c7f27..b0e9a96c 100644 --- a/cspell.json +++ b/cspell.json @@ -9,15 +9,17 @@ "douyinfe", "flowgram", "flowgram.ai", + "gedit", "Hoverable", + "langchain", "openbracket", "rsbuild", "rspack", "rspress", "Sandpack", + "testrun", "zoomin", - "zoomout", - "gedit" + "zoomout" ], "ignoreWords": [], "import": [] diff --git a/packages/plugins/free-container-plugin/src/sub-canvas/components/render/index.tsx b/packages/plugins/free-container-plugin/src/sub-canvas/components/render/index.tsx index fd109ecc..f4c6a7a1 100644 --- a/packages/plugins/free-container-plugin/src/sub-canvas/components/render/index.tsx +++ b/packages/plugins/free-container-plugin/src/sub-canvas/components/render/index.tsx @@ -1,23 +1,20 @@ import React, { CSSProperties, type FC } from 'react'; -import { useCurrentEntity } from '@flowgram.ai/free-layout-core'; - import { SubCanvasRenderStyle } from './style'; import { SubCanvasTips } from '../tips'; import { SubCanvasBorder } from '../border'; import { SubCanvasBackground } from '../background'; import { useNodeSize, useSyncNodeRenderSize } from '../../hooks'; -interface ISubCanvasBorder { +interface ISubCanvasRender { + offsetY: number; className?: string; style?: CSSProperties; } -export const SubCanvasRender: FC = ({ className, style }) => { - const node = useCurrentEntity(); +export const SubCanvasRender: FC = ({ className, style, offsetY }) => { const nodeSize = useNodeSize(); const nodeHeight = nodeSize?.height ?? 0; - const { padding } = node.transform; useSyncNodeRenderSize(nodeSize); @@ -25,7 +22,7 @@ export const SubCanvasRender: FC = ({ className, style }) => { Promise; + [FlowGramAPIName.TaskReport]: (input: TaskReportInput) => Promise; + [FlowGramAPIName.TaskResult]: (input: TaskResultInput) => Promise; + [FlowGramAPIName.TaskCancel]: (input: TaskCancelInput) => Promise; +} diff --git a/packages/runtime/interface/src/index.ts b/packages/runtime/interface/src/index.ts new file mode 100644 index 00000000..2436056f --- /dev/null +++ b/packages/runtime/interface/src/index.ts @@ -0,0 +1,5 @@ +export * from './api'; +export * from './schema'; +export * from './node'; +export * from './runtime'; +export * from './client'; diff --git a/packages/runtime/interface/src/node/constant.ts b/packages/runtime/interface/src/node/constant.ts new file mode 100644 index 00000000..5b10342d --- /dev/null +++ b/packages/runtime/interface/src/node/constant.ts @@ -0,0 +1,11 @@ +export enum FlowGramNode { + Root = 'root', + Start = 'start', + End = 'end', + LLM = 'llm', + code = 'code', + Condition = 'condition', + Loop = 'loop', + Comment = 'comment', + Group = 'group', +} diff --git a/packages/runtime/interface/src/node/end/index.ts b/packages/runtime/interface/src/node/end/index.ts new file mode 100644 index 00000000..d078fe2f --- /dev/null +++ b/packages/runtime/interface/src/node/end/index.ts @@ -0,0 +1,13 @@ +import { IFlowConstantRefValue } from '@schema/value'; +import { WorkflowNodeSchema } from '@schema/node'; +import { IJsonSchema } from '@schema/json-schema'; +import { FlowGramNode } from '@node/constant'; + +interface EndNodeData { + title: string; + inputs: IJsonSchema<'object'>; + outputs: IJsonSchema<'object'>; + outputValues: Record; +} + +export type EndNodeSchema = WorkflowNodeSchema; diff --git a/packages/runtime/interface/src/node/index.ts b/packages/runtime/interface/src/node/index.ts new file mode 100644 index 00000000..d5377b2a --- /dev/null +++ b/packages/runtime/interface/src/node/index.ts @@ -0,0 +1,4 @@ +export { FlowGramNode } from './constant'; +export { EndNodeSchema } from './end'; +export { LLMNodeSchema } from './llm'; +export { StartNodeSchema } from './start'; diff --git a/packages/runtime/interface/src/node/llm/index.ts b/packages/runtime/interface/src/node/llm/index.ts new file mode 100644 index 00000000..3bf87f3d --- /dev/null +++ b/packages/runtime/interface/src/node/llm/index.ts @@ -0,0 +1,20 @@ +import { IFlowConstantRefValue } from '@schema/value'; +import { WorkflowNodeSchema } from '@schema/node'; +import { IJsonSchema } from '@schema/json-schema'; +import { FlowGramNode } from '@node/constant'; + +interface LLMNodeData { + title: string; + inputs: IJsonSchema<'object'>; + outputs: IJsonSchema<'object'>; + inputValues: { + apiKey: IFlowConstantRefValue; + modelType: IFlowConstantRefValue; + baseURL: IFlowConstantRefValue; + temperature: IFlowConstantRefValue; + systemPrompt: IFlowConstantRefValue; + prompt: IFlowConstantRefValue; + }; +} + +export type LLMNodeSchema = WorkflowNodeSchema; diff --git a/packages/runtime/interface/src/node/start/index.ts b/packages/runtime/interface/src/node/start/index.ts new file mode 100644 index 00000000..f0dc5b86 --- /dev/null +++ b/packages/runtime/interface/src/node/start/index.ts @@ -0,0 +1,10 @@ +import { WorkflowNodeSchema } from '@schema/node'; +import { IJsonSchema } from '@schema/json-schema'; +import { FlowGramNode } from '@node/constant'; + +interface StartNodeData { + title: string; + outputs: IJsonSchema<'object'>; +} + +export type StartNodeSchema = WorkflowNodeSchema; diff --git a/packages/runtime/interface/src/runtime/base/index.ts b/packages/runtime/interface/src/runtime/base/index.ts new file mode 100644 index 00000000..e5f0222a --- /dev/null +++ b/packages/runtime/interface/src/runtime/base/index.ts @@ -0,0 +1,3 @@ +export type { VOData } from './value-object'; +export type { InvokeParams, WorkflowRuntimeInvoke } from './invoke'; +export type { WorkflowInputs, WorkflowOutputs } from './inputs-outputs'; diff --git a/packages/runtime/interface/src/runtime/base/inputs-outputs.ts b/packages/runtime/interface/src/runtime/base/inputs-outputs.ts new file mode 100644 index 00000000..a2b9cfda --- /dev/null +++ b/packages/runtime/interface/src/runtime/base/inputs-outputs.ts @@ -0,0 +1,2 @@ +export type WorkflowInputs = Record; +export type WorkflowOutputs = Record; diff --git a/packages/runtime/interface/src/runtime/base/invoke.ts b/packages/runtime/interface/src/runtime/base/invoke.ts new file mode 100644 index 00000000..da68cdd8 --- /dev/null +++ b/packages/runtime/interface/src/runtime/base/invoke.ts @@ -0,0 +1,9 @@ +import { WorkflowSchema } from '@schema/index'; +import { WorkflowInputs } from './inputs-outputs'; + +export interface InvokeParams { + schema: WorkflowSchema; + inputs: WorkflowInputs; +} + +export type WorkflowRuntimeInvoke = (params: InvokeParams) => Promise; diff --git a/packages/runtime/interface/src/runtime/base/value-object.ts b/packages/runtime/interface/src/runtime/base/value-object.ts new file mode 100644 index 00000000..cf5053a4 --- /dev/null +++ b/packages/runtime/interface/src/runtime/base/value-object.ts @@ -0,0 +1 @@ +export type VOData = Omit; diff --git a/packages/runtime/interface/src/runtime/container/index.ts b/packages/runtime/interface/src/runtime/container/index.ts new file mode 100644 index 00000000..3bcb2e4b --- /dev/null +++ b/packages/runtime/interface/src/runtime/container/index.ts @@ -0,0 +1,5 @@ +export type ContainerService = any; + +export interface IContainer { + get(key: any): T; +} diff --git a/packages/runtime/interface/src/runtime/context/index.ts b/packages/runtime/interface/src/runtime/context/index.ts new file mode 100644 index 00000000..44ee0ea8 --- /dev/null +++ b/packages/runtime/interface/src/runtime/context/index.ts @@ -0,0 +1,25 @@ +import { IVariableStore } from '@runtime/variable'; +import { IStatusCenter } from '@runtime/status'; +import { ISnapshotCenter } from '@runtime/snapshot'; +import { IIOCenter } from '@runtime/io-center'; +import { IState } from '../state'; +import { IReporter } from '../reporter'; +import { IDocument } from '../document'; +import { InvokeParams } from '../base'; + +export interface ContextData { + variableStore: IVariableStore; + state: IState; + document: IDocument; + ioCenter: IIOCenter; + snapshotCenter: ISnapshotCenter; + statusCenter: IStatusCenter; + reporter: IReporter; +} + +export interface IContext extends ContextData { + id: string; + init(params: InvokeParams): void; + dispose(): void; + sub(): IContext; +} diff --git a/packages/runtime/interface/src/runtime/document/document.ts b/packages/runtime/interface/src/runtime/document/document.ts new file mode 100644 index 00000000..52ff19ed --- /dev/null +++ b/packages/runtime/interface/src/runtime/document/document.ts @@ -0,0 +1,14 @@ +import { WorkflowSchema } from '@schema/index'; +import { INode } from './node'; +import { IEdge } from './edge'; + +export interface IDocument { + id: string; + nodes: INode[]; + edges: IEdge[]; + root: INode; + start: INode; + end: INode; + init(schema: WorkflowSchema): void; + dispose(): void; +} diff --git a/packages/runtime/interface/src/runtime/document/edge.ts b/packages/runtime/interface/src/runtime/document/edge.ts new file mode 100644 index 00000000..aaa0858e --- /dev/null +++ b/packages/runtime/interface/src/runtime/document/edge.ts @@ -0,0 +1,16 @@ +import { IPort } from './port'; +import { INode } from './node'; + +export interface IEdge { + id: string; + from: INode; + to: INode; + fromPort: IPort; + toPort: IPort; +} + +export interface CreateEdgeParams { + id: string; + from: INode; + to: INode; +} diff --git a/packages/runtime/interface/src/runtime/document/index.ts b/packages/runtime/interface/src/runtime/document/index.ts new file mode 100644 index 00000000..486ec297 --- /dev/null +++ b/packages/runtime/interface/src/runtime/document/index.ts @@ -0,0 +1,4 @@ +export type { IDocument } from './document'; +export type { IEdge, CreateEdgeParams } from './edge'; +export type { NodeDeclare as NodeVariable, INode, CreateNodeParams } from './node'; +export type { IPort, CreatePortParams } from './port'; diff --git a/packages/runtime/interface/src/runtime/document/node.ts b/packages/runtime/interface/src/runtime/document/node.ts new file mode 100644 index 00000000..db3a3c77 --- /dev/null +++ b/packages/runtime/interface/src/runtime/document/node.ts @@ -0,0 +1,41 @@ +import { IFlowConstantRefValue, IJsonSchema, PositionSchema } from '@schema/index'; +import { FlowGramNode } from '@node/constant'; +import { IPort } from './port'; +import { IEdge } from './edge'; + +export interface NodeDeclare { + inputsValues?: Record; + inputs?: IJsonSchema; + outputs?: IJsonSchema; +} + +export interface INode { + id: string; + type: FlowGramNode; + name: string; + position: PositionSchema; + declare: NodeDeclare; + data: T; + ports: { + inputs: IPort[]; + outputs: IPort[]; + }; + edges: { + inputs: IEdge[]; + outputs: IEdge[]; + }; + parent: INode | null; + children: INode[]; + prev: INode[]; + next: INode[]; + isBranch: boolean; +} + +export interface CreateNodeParams { + id: string; + type: FlowGramNode; + name: string; + position: PositionSchema; + variable?: NodeDeclare; + data?: any; +} diff --git a/packages/runtime/interface/src/runtime/document/port.ts b/packages/runtime/interface/src/runtime/document/port.ts new file mode 100644 index 00000000..099ea291 --- /dev/null +++ b/packages/runtime/interface/src/runtime/document/port.ts @@ -0,0 +1,16 @@ +import { WorkflowPortType } from '@schema/index'; +import { INode } from './node'; +import { IEdge } from './edge'; + +export interface IPort { + id: string; + node: INode; + edges: IEdge[]; + type: WorkflowPortType; +} + +export interface CreatePortParams { + id: string; + node: INode; + type: WorkflowPortType; +} diff --git a/packages/runtime/interface/src/runtime/engine/index.ts b/packages/runtime/interface/src/runtime/engine/index.ts new file mode 100644 index 00000000..7fa0c6da --- /dev/null +++ b/packages/runtime/interface/src/runtime/engine/index.ts @@ -0,0 +1,16 @@ +import { ITask } from '../task'; +import { IExecutor } from '../executor'; +import { INode } from '../document'; +import { IContext } from '../context'; +import { InvokeParams } from '../base'; + +export interface EngineServices { + Executor: IExecutor; +} + +export interface IEngine { + invoke(params: InvokeParams): ITask; + executeNode(params: { context: IContext; node: INode }): Promise; +} + +export const IEngine = Symbol.for('Engine'); diff --git a/packages/runtime/interface/src/runtime/executor/executor.ts b/packages/runtime/interface/src/runtime/executor/executor.ts new file mode 100644 index 00000000..874d62b1 --- /dev/null +++ b/packages/runtime/interface/src/runtime/executor/executor.ts @@ -0,0 +1,8 @@ +import { ExecutionContext, ExecutionResult, INodeExecutor } from './node-executor'; + +export interface IExecutor { + execute: (context: ExecutionContext) => Promise; + register: (executor: INodeExecutor) => void; +} + +export const IExecutor = Symbol.for('Executor'); diff --git a/packages/runtime/interface/src/runtime/executor/index.ts b/packages/runtime/interface/src/runtime/executor/index.ts new file mode 100644 index 00000000..3830c4c9 --- /dev/null +++ b/packages/runtime/interface/src/runtime/executor/index.ts @@ -0,0 +1,7 @@ +export { IExecutor } from './executor'; +export type { + ExecutionContext, + ExecutionResult, + INodeExecutor, + INodeExecutorFactory, +} from './node-executor'; diff --git a/packages/runtime/interface/src/runtime/executor/node-executor.ts b/packages/runtime/interface/src/runtime/executor/node-executor.ts new file mode 100644 index 00000000..0ade2c9c --- /dev/null +++ b/packages/runtime/interface/src/runtime/executor/node-executor.ts @@ -0,0 +1,26 @@ +import { FlowGramNode } from '@node/index'; +import { INode } from '../document'; +import { IContext } from '../context'; +import { IContainer } from '../container'; +import { WorkflowInputs, WorkflowOutputs } from '../base'; + +export interface ExecutionContext { + node: INode; + inputs: WorkflowInputs; + container: IContainer; + runtime: IContext; +} + +export interface ExecutionResult { + outputs: WorkflowOutputs; + branch?: string; +} + +export interface INodeExecutor { + type: FlowGramNode; + execute: (context: ExecutionContext) => Promise; +} + +export interface INodeExecutorFactory { + new (): INodeExecutor; +} diff --git a/packages/runtime/interface/src/runtime/index.ts b/packages/runtime/interface/src/runtime/index.ts new file mode 100644 index 00000000..941163e8 --- /dev/null +++ b/packages/runtime/interface/src/runtime/index.ts @@ -0,0 +1,14 @@ +export * from './container'; +export * from './base'; +export * from './engine'; +export * from './context'; +export * from './document'; +export * from './executor'; +export * from './io-center'; +export * from './snapshot'; +export * from './reporter'; +export * from './state'; +export * from './status'; +export * from './task'; +export * from './validation'; +export * from './variable'; diff --git a/packages/runtime/interface/src/runtime/io-center/index.ts b/packages/runtime/interface/src/runtime/io-center/index.ts new file mode 100644 index 00000000..8de30c25 --- /dev/null +++ b/packages/runtime/interface/src/runtime/io-center/index.ts @@ -0,0 +1,17 @@ +import { WorkflowInputs, WorkflowOutputs } from '../base'; + +export interface IOData { + inputs: WorkflowInputs; + outputs: WorkflowOutputs; +} + +/** Input & Output */ +export interface IIOCenter { + inputs: WorkflowInputs; + outputs: WorkflowOutputs; + setInputs(inputs: WorkflowInputs): void; + setOutputs(outputs: WorkflowOutputs): void; + init(inputs: WorkflowInputs): void; + dispose(): void; + export(): IOData; +} diff --git a/packages/runtime/interface/src/runtime/reporter/index.ts b/packages/runtime/interface/src/runtime/reporter/index.ts new file mode 100644 index 00000000..30ad917f --- /dev/null +++ b/packages/runtime/interface/src/runtime/reporter/index.ts @@ -0,0 +1,23 @@ +import { StatusData, IStatusCenter } from '../status'; +import { Snapshot, ISnapshotCenter } from '../snapshot'; +import { WorkflowInputs, WorkflowOutputs } from '../base'; +export interface NodeReport extends StatusData { + id: string; + snapshots: Snapshot[]; +} + +export interface IReport { + id: string; + inputs: WorkflowInputs; + outputs: WorkflowOutputs; + workflowStatus: StatusData; + reports: Record; +} + +export interface IReporter { + snapshotCenter: ISnapshotCenter; + statusCenter: IStatusCenter; + init(): void; + dispose(): void; + export(): IReport; +} diff --git a/packages/runtime/interface/src/runtime/snapshot/index.ts b/packages/runtime/interface/src/runtime/snapshot/index.ts new file mode 100644 index 00000000..11970c14 --- /dev/null +++ b/packages/runtime/interface/src/runtime/snapshot/index.ts @@ -0,0 +1,2 @@ +export type { ISnapshot, Snapshot, SnapshotData } from './snapshot'; +export type { ISnapshotCenter } from './snapshot-center'; diff --git a/packages/runtime/interface/src/runtime/snapshot/snapshot-center.ts b/packages/runtime/interface/src/runtime/snapshot/snapshot-center.ts new file mode 100644 index 00000000..b98fe540 --- /dev/null +++ b/packages/runtime/interface/src/runtime/snapshot/snapshot-center.ts @@ -0,0 +1,10 @@ +import { ISnapshot, Snapshot, SnapshotData } from './snapshot'; + +export interface ISnapshotCenter { + id: string; + create(snapshot: Partial): ISnapshot; + exportAll(): Snapshot[]; + export(): Record; + init(): void; + dispose(): void; +} diff --git a/packages/runtime/interface/src/runtime/snapshot/snapshot.ts b/packages/runtime/interface/src/runtime/snapshot/snapshot.ts new file mode 100644 index 00000000..8392ddcc --- /dev/null +++ b/packages/runtime/interface/src/runtime/snapshot/snapshot.ts @@ -0,0 +1,21 @@ +import { WorkflowInputs, WorkflowOutputs } from '../base'; + +export interface SnapshotData { + nodeID: string; + inputs: WorkflowInputs; + outputs: WorkflowOutputs; + data: any; + branch?: string; +} + +export interface Snapshot extends SnapshotData { + id: string; +} + +export interface ISnapshot { + id: string; + data: Partial; + addData(data: Partial): void; + validate(): boolean; + export(): Snapshot; +} diff --git a/packages/runtime/interface/src/runtime/state/index.ts b/packages/runtime/interface/src/runtime/state/index.ts new file mode 100644 index 00000000..be177b3e --- /dev/null +++ b/packages/runtime/interface/src/runtime/state/index.ts @@ -0,0 +1,20 @@ +import { IFlowConstantRefValue, IFlowRefValue, WorkflowVariableType } from '@schema/index'; +import { IVariableParseResult, IVariableStore } from '../variable'; +import { INode } from '../document'; +import { WorkflowInputs, WorkflowOutputs } from '../base'; + +export interface IState { + id: string; + variableStore: IVariableStore; + init(): void; + dispose(): void; + getNodeInputs(node: INode): WorkflowInputs; + setNodeOutputs(params: { node: INode; outputs: WorkflowOutputs }): void; + parseRef(ref: IFlowRefValue): IVariableParseResult | null; + parseValue( + flowValue: IFlowConstantRefValue, + type?: WorkflowVariableType + ): IVariableParseResult | null; + isExecutedNode(node: INode): boolean; + addExecutedNode(node: INode): void; +} diff --git a/packages/runtime/interface/src/runtime/status/index.ts b/packages/runtime/interface/src/runtime/status/index.ts new file mode 100644 index 00000000..c79031d5 --- /dev/null +++ b/packages/runtime/interface/src/runtime/status/index.ts @@ -0,0 +1,33 @@ +export enum WorkflowStatus { + Pending = 'pending', + Processing = 'processing', + Succeeded = 'succeeded', + Failed = 'failed', + Canceled = 'canceled', +} + +export interface StatusData { + status: WorkflowStatus; + terminated: boolean; + startTime: number; + endTime?: number; + timeCost: number; +} + +export interface IStatus extends StatusData { + id: string; + process(): void; + success(): void; + fail(): void; + cancel(): void; + export(): StatusData; +} + +export interface IStatusCenter { + workflow: IStatus; + nodeStatus(nodeID: string): IStatus; + init(): void; + dispose(): void; + getStatusNodeIDs(status: WorkflowStatus): string[]; + exportNodeStatus(): Record; +} diff --git a/packages/runtime/interface/src/runtime/task/index.ts b/packages/runtime/interface/src/runtime/task/index.ts new file mode 100644 index 00000000..2edaefef --- /dev/null +++ b/packages/runtime/interface/src/runtime/task/index.ts @@ -0,0 +1,14 @@ +import { IContext } from '../context'; +import { WorkflowOutputs } from '../base'; + +export interface ITask { + id: string; + processing: Promise; + context: IContext; + cancel(): void; +} + +export interface TaskParams { + processing: Promise; + context: IContext; +} diff --git a/packages/runtime/interface/src/runtime/validation/index.ts b/packages/runtime/interface/src/runtime/validation/index.ts new file mode 100644 index 00000000..e8c5ca42 --- /dev/null +++ b/packages/runtime/interface/src/runtime/validation/index.ts @@ -0,0 +1,12 @@ +import { WorkflowSchema } from '@schema/index'; + +export interface ValidationResult { + valid: boolean; + errors?: string[]; +} + +export interface IValidation { + validate(schema: WorkflowSchema): ValidationResult; +} + +export const IValidation = Symbol.for('Validation'); diff --git a/packages/runtime/interface/src/runtime/variable/index.ts b/packages/runtime/interface/src/runtime/variable/index.ts new file mode 100644 index 00000000..2d4b821a --- /dev/null +++ b/packages/runtime/interface/src/runtime/variable/index.ts @@ -0,0 +1,44 @@ +import { WorkflowVariableType } from '@schema/index'; + +interface VariableTypeInfo { + type: WorkflowVariableType; + itemsType?: WorkflowVariableType; +} + +export interface IVariable extends VariableTypeInfo { + id: string; + nodeID: string; + key: string; + value: T; +} + +export interface IVariableParseResult extends VariableTypeInfo { + value: T; + type: WorkflowVariableType; +} + +export interface IVariableStore { + id: string; + store: Map>; + setParent(parent: IVariableStore): void; + setVariable( + params: { + nodeID: string; + key: string; + value: Object; + } & VariableTypeInfo + ): void; + setValue(params: { + nodeID: string; + variableKey: string; + variablePath?: string[]; + value: Object; + }): void; + getValue(params: { + nodeID: string; + variableKey: string; + variablePath?: string[]; + }): IVariableParseResult | null; + init(): void; + dispose(): void; +} diff --git a/packages/runtime/interface/src/schema/constant.ts b/packages/runtime/interface/src/schema/constant.ts new file mode 100644 index 00000000..329e9ab8 --- /dev/null +++ b/packages/runtime/interface/src/schema/constant.ts @@ -0,0 +1,14 @@ +export enum WorkflowPortType { + Input = 'input', + Output = 'output', +} + +export enum WorkflowVariableType { + String = 'string', + Integer = 'integer', + Number = 'number', + Boolean = 'boolean', + Object = 'object', + Array = 'array', + Null = 'null', +} diff --git a/packages/runtime/interface/src/schema/edge.ts b/packages/runtime/interface/src/schema/edge.ts new file mode 100644 index 00000000..0fae12a3 --- /dev/null +++ b/packages/runtime/interface/src/schema/edge.ts @@ -0,0 +1,6 @@ +export interface WorkflowEdgeSchema { + sourceNodeID: string; + targetNodeID: string; + sourcePortID?: string; + targetPortID?: string; +} diff --git a/packages/runtime/interface/src/schema/index.ts b/packages/runtime/interface/src/schema/index.ts new file mode 100644 index 00000000..5db4878a --- /dev/null +++ b/packages/runtime/interface/src/schema/index.ts @@ -0,0 +1,8 @@ +export { WorkflowEdgeSchema } from './edge'; +export { JsonSchemaBasicType, IJsonSchema, IBasicJsonSchema } from './json-schema'; +export { WorkflowNodeMetaSchema } from './node-meta'; +export { WorkflowNodeSchema } from './node'; +export { WorkflowSchema } from './workflow'; +export { XYSchema, PositionSchema } from './xy'; +export { WorkflowPortType, WorkflowVariableType } from './constant'; +export { IFlowConstantRefValue, IFlowConstantValue, IFlowRefValue } from './value'; diff --git a/packages/runtime/interface/src/schema/json-schema.ts b/packages/runtime/interface/src/schema/json-schema.ts new file mode 100644 index 00000000..7626535e --- /dev/null +++ b/packages/runtime/interface/src/schema/json-schema.ts @@ -0,0 +1,33 @@ +// TODO copy packages/materials/form-materials/src/typings/json-schema/index.ts + +export type JsonSchemaBasicType = + | 'boolean' + | 'string' + | 'integer' + | 'number' + | 'object' + | 'array' + | 'map'; + +export interface IJsonSchema { + type: T; + default?: any; + title?: string; + description?: string; + enum?: (string | number)[]; + properties?: Record>; + additionalProperties?: IJsonSchema; + items?: IJsonSchema; + required?: string[]; + $ref?: string; + extra?: { + index?: number; + // Used in BaseType.isEqualWithJSONSchema, the type comparison will be weak + weak?: boolean; + // Set the render component + formComponent?: string; + [key: string]: any; + }; +} + +export type IBasicJsonSchema = IJsonSchema; diff --git a/packages/runtime/interface/src/schema/node-meta.ts b/packages/runtime/interface/src/schema/node-meta.ts new file mode 100644 index 00000000..67241104 --- /dev/null +++ b/packages/runtime/interface/src/schema/node-meta.ts @@ -0,0 +1,6 @@ +import { PositionSchema } from './xy'; + +export interface WorkflowNodeMetaSchema { + position: PositionSchema; + canvasPosition?: PositionSchema; +} diff --git a/packages/runtime/interface/src/schema/node.ts b/packages/runtime/interface/src/schema/node.ts new file mode 100644 index 00000000..e17ba8f0 --- /dev/null +++ b/packages/runtime/interface/src/schema/node.ts @@ -0,0 +1,19 @@ +import type { IFlowConstantRefValue } from './value'; +import type { WorkflowNodeMetaSchema } from './node-meta'; +import { IJsonSchema } from './json-schema'; +import type { WorkflowEdgeSchema } from './edge'; + +export interface WorkflowNodeSchema { + id: string; + type: T; + meta: WorkflowNodeMetaSchema; + data: D & { + title?: string; + inputsValues?: Record; + inputs?: IJsonSchema; + outputs?: IJsonSchema; + [key: string]: any; + }; + blocks?: WorkflowNodeSchema[]; + edges?: WorkflowEdgeSchema[]; +} diff --git a/packages/runtime/interface/src/schema/value.ts b/packages/runtime/interface/src/schema/value.ts new file mode 100644 index 00000000..406eb60a --- /dev/null +++ b/packages/runtime/interface/src/schema/value.ts @@ -0,0 +1,29 @@ +// TODO copy packages/materials/form-materials/src/typings/flow-value/index.ts + +export interface IFlowConstantValue { + type: 'constant'; + content?: string | number | boolean; +} + +export interface IFlowRefValue { + type: 'ref'; + content?: string[]; +} + +export interface IFlowExpressionValue { + type: 'expression'; + content?: string; +} + +export interface IFlowTemplateValue { + type: 'template'; + content?: string; +} + +export type IFlowValue = + | IFlowConstantValue + | IFlowRefValue + | IFlowExpressionValue + | IFlowTemplateValue; + +export type IFlowConstantRefValue = IFlowConstantValue | IFlowRefValue; diff --git a/packages/runtime/interface/src/schema/workflow.ts b/packages/runtime/interface/src/schema/workflow.ts new file mode 100644 index 00000000..91846c9f --- /dev/null +++ b/packages/runtime/interface/src/schema/workflow.ts @@ -0,0 +1,7 @@ +import type { WorkflowNodeSchema } from './node'; +import type { WorkflowEdgeSchema } from './edge'; + +export interface WorkflowSchema { + nodes: WorkflowNodeSchema[]; + edges: WorkflowEdgeSchema[]; +} diff --git a/packages/runtime/interface/src/schema/xy.ts b/packages/runtime/interface/src/schema/xy.ts new file mode 100644 index 00000000..7cbe86b1 --- /dev/null +++ b/packages/runtime/interface/src/schema/xy.ts @@ -0,0 +1,6 @@ +export interface XYSchema { + x: number; + y: number; +} + +export type PositionSchema = XYSchema; diff --git a/packages/runtime/interface/tsconfig.json b/packages/runtime/interface/tsconfig.json new file mode 100644 index 00000000..d97bca09 --- /dev/null +++ b/packages/runtime/interface/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json", + "compilerOptions": { + "baseUrl": "src", + "paths": { + "@api/*": [ + "api/*" + ], + "@node/*": [ + "node/*" + ], + "@runtime/*": [ + "runtime/*" + ], + "@schema/*": [ + "schema/*" + ], + "@client/*": [ + "client/*" + ] + } + }, + "include": [ + "./src" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/runtime/js-core/.eslintrc.cjs b/packages/runtime/js-core/.eslintrc.cjs new file mode 100644 index 00000000..b1ea5bf8 --- /dev/null +++ b/packages/runtime/js-core/.eslintrc.cjs @@ -0,0 +1,6 @@ +const { defineConfig } = require('@flowgram.ai/eslint-config'); + +module.exports = defineConfig({ + preset: 'base', + packageRoot: __dirname, +}); diff --git a/packages/runtime/js-core/package.json b/packages/runtime/js-core/package.json new file mode 100644 index 00000000..db1f2cbf --- /dev/null +++ b/packages/runtime/js-core/package.json @@ -0,0 +1,53 @@ +{ + "name": "@flowgram.ai/runtime-js", + "version": "0.1.8", + "homepage": "https://flowgram.ai/", + "repository": "https://github.com/bytedance/flowgram.ai", + "license": "MIT", + "exports": { + "types": "./dist/index.d.ts", + "import": "./dist/esm/index.js", + "require": "./dist/index.js" + }, + "main": "./dist/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "dev": "npm run watch", + "build": "npm run build:fast -- --dts-resolve", + "build:fast": "tsup src/index.ts --format cjs,esm --sourcemap --legacy-output", + "build:watch": "npm run build:fast -- --dts-resolve", + "clean": "rimraf dist", + "test": "vitest run", + "test:cov": "vitest run --coverage", + "ts-check": "tsc --noEmit", + "watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist" + }, + "dependencies": { + "@langchain/openai": "^0.5.11", + "@langchain/core": "^0.3.57", + "lodash-es": "^4.17.21", + "uuid": "^9.0.0", + "zod": "^3.24.4" + }, + "devDependencies": { + "@flowgram.ai/runtime-interface": "workspace:*", + "@flowgram.ai/eslint-config": "workspace:*", + "@flowgram.ai/ts-config": "workspace:*", + "@types/lodash-es": "^4.17.12", + "@types/uuid": "^9.0.1", + "@vitest/coverage-v8": "^0.32.0", + "eslint": "^8.54.0", + "dotenv": "~16.5.0", + "tsup": "^8.0.1", + "typescript": "^5.0.4", + "vitest": "^0.34.6" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/runtime/js-core/src/api/index.ts b/packages/runtime/js-core/src/api/index.ts new file mode 100644 index 00000000..92551ddb --- /dev/null +++ b/packages/runtime/js-core/src/api/index.ts @@ -0,0 +1,17 @@ +import { FlowGramAPIName } from '@flowgram.ai/runtime-interface'; + +import { TaskRunAPI } from './task-run'; +import { TaskResultAPI } from './task-result'; +import { TaskReportAPI } from './task-report'; +import { TaskCancelAPI } from './task-cancel'; + +export { TaskRunAPI, TaskResultAPI, TaskReportAPI, TaskCancelAPI }; + +export const WorkflowRuntimeAPIs: Record any> = { + [FlowGramAPIName.TaskRun]: TaskRunAPI, + [FlowGramAPIName.TaskReport]: TaskReportAPI, + [FlowGramAPIName.TaskResult]: TaskResultAPI, + [FlowGramAPIName.TaskCancel]: TaskCancelAPI, + [FlowGramAPIName.ServerInfo]: () => {}, // TODO + [FlowGramAPIName.Validation]: () => {}, // TODO +}; diff --git a/packages/runtime/js-core/src/api/task-cancel.ts b/packages/runtime/js-core/src/api/task-cancel.ts new file mode 100644 index 00000000..48fb31cc --- /dev/null +++ b/packages/runtime/js-core/src/api/task-cancel.ts @@ -0,0 +1,13 @@ +import { TaskCancelInput, TaskCancelOutput } from '@flowgram.ai/runtime-interface'; + +import { WorkflowApplication } from '@application/workflow'; + +export const TaskCancelAPI = async (input: TaskCancelInput): Promise => { + const app = WorkflowApplication.instance; + const { taskID } = input; + const success = app.cancel(taskID); + const output: TaskCancelOutput = { + success, + }; + return output; +}; diff --git a/packages/runtime/js-core/src/api/task-report.ts b/packages/runtime/js-core/src/api/task-report.ts new file mode 100644 index 00000000..ddc2477b --- /dev/null +++ b/packages/runtime/js-core/src/api/task-report.ts @@ -0,0 +1,21 @@ +/* eslint-disable no-console */ +import { + TaskReportInput, + TaskReportOutput, + TaskReportDefine, +} from '@flowgram.ai/runtime-interface'; + +import { WorkflowApplication } from '@application/workflow'; + +export const TaskReportAPI = async (input: TaskReportInput): Promise => { + const app = WorkflowApplication.instance; + const { taskID } = input; + const output: TaskReportOutput = app.report(taskID); + try { + TaskReportDefine.schema.output.parse(output); + } catch (e) { + console.log('> TaskReportAPI - output: ', JSON.stringify(output)); + console.error(e); + } + return output; +}; diff --git a/packages/runtime/js-core/src/api/task-result.ts b/packages/runtime/js-core/src/api/task-result.ts new file mode 100644 index 00000000..250b9d83 --- /dev/null +++ b/packages/runtime/js-core/src/api/task-result.ts @@ -0,0 +1,10 @@ +import { TaskResultInput, TaskResultOutput } from '@flowgram.ai/runtime-interface'; + +import { WorkflowApplication } from '@application/workflow'; + +export const TaskResultAPI = async (input: TaskResultInput): Promise => { + const app = WorkflowApplication.instance; + const { taskID } = input; + const output: TaskResultOutput = app.result(taskID); + return output; +}; diff --git a/packages/runtime/js-core/src/api/task-run.ts b/packages/runtime/js-core/src/api/task-run.ts new file mode 100644 index 00000000..6f38661c --- /dev/null +++ b/packages/runtime/js-core/src/api/task-run.ts @@ -0,0 +1,17 @@ +import { TaskRunInput, TaskRunOutput } from '@flowgram.ai/runtime-interface'; + +import { WorkflowApplication } from '@application/workflow'; + +export const TaskRunAPI = async (input: TaskRunInput): Promise => { + const app = WorkflowApplication.instance; + const { schema: stringSchema, inputs } = input; + const schema = JSON.parse(stringSchema); + const taskID = app.run({ + schema, + inputs, + }); + const output: TaskRunOutput = { + taskID, + }; + return output; +}; diff --git a/packages/runtime/js-core/src/application/index.ts b/packages/runtime/js-core/src/application/index.ts new file mode 100644 index 00000000..4c54593e --- /dev/null +++ b/packages/runtime/js-core/src/application/index.ts @@ -0,0 +1 @@ +export { WorkflowApplication } from './workflow'; diff --git a/packages/runtime/js-core/src/application/workflow.ts b/packages/runtime/js-core/src/application/workflow.ts new file mode 100644 index 00000000..852bb58c --- /dev/null +++ b/packages/runtime/js-core/src/application/workflow.ts @@ -0,0 +1,76 @@ +/* eslint-disable no-console */ +import { + InvokeParams, + IContainer, + IEngine, + ITask, + IReport, + WorkflowOutputs, +} from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeContainer } from '@workflow/container'; + +export class WorkflowApplication { + private container: IContainer; + + public tasks: Map; + + constructor() { + this.container = WorkflowRuntimeContainer.instance; + this.tasks = new Map(); + } + + public run(params: InvokeParams): string { + const engine = this.container.get(IEngine); + const task = engine.invoke(params); + this.tasks.set(task.id, task); + console.log('> POST TaskRun - taskID: ', task.id); + console.log(params.inputs); + task.processing.then((output) => { + console.log('> LOG Task finished: ', task.id); + console.log(output); + }); + return task.id; + } + + public cancel(taskID: string): boolean { + console.log('> PUT TaskCancel - taskID: ', taskID); + const task = this.tasks.get(taskID); + if (!task) { + return false; + } + task.cancel(); + return true; + } + + public report(taskID: string): IReport | undefined { + const task = this.tasks.get(taskID); + console.log('> GET TaskReport - taskID: ', taskID); + if (!task) { + return; + } + return task.context.reporter.export(); + } + + public result(taskID: string): WorkflowOutputs | undefined { + console.log('> GET TaskResult - taskID: ', taskID); + const task = this.tasks.get(taskID); + if (!task) { + return; + } + if (!task.context.statusCenter.workflow.terminated) { + return; + } + return task.context.ioCenter.outputs; + } + + private static _instance: WorkflowApplication; + + public static get instance(): WorkflowApplication { + if (this._instance) { + return this._instance; + } + this._instance = new WorkflowApplication(); + return this._instance; + } +} diff --git a/packages/runtime/js-core/src/domain/__tests__/config.ts b/packages/runtime/js-core/src/domain/__tests__/config.ts new file mode 100644 index 00000000..c15cd66b --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/config.ts @@ -0,0 +1 @@ +export const ENABLE_REAL_LLM = false; diff --git a/packages/runtime/js-core/src/domain/__tests__/executor/index.ts b/packages/runtime/js-core/src/domain/__tests__/executor/index.ts new file mode 100644 index 00000000..32d19fbc --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/executor/index.ts @@ -0,0 +1,13 @@ +import { INodeExecutorFactory } from '@flowgram.ai/runtime-interface'; + +import { StartExecutor } from '@nodes/start'; +import { EndExecutor } from '@nodes/end'; +import { ConditionExecutor } from '@nodes/condition'; +import { MockLLMExecutor } from './llm'; + +export const MockWorkflowRuntimeNodeExecutors: INodeExecutorFactory[] = [ + StartExecutor, + EndExecutor, + MockLLMExecutor, + ConditionExecutor, +]; diff --git a/packages/runtime/js-core/src/domain/__tests__/executor/llm.ts b/packages/runtime/js-core/src/domain/__tests__/executor/llm.ts new file mode 100644 index 00000000..a47f7772 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/executor/llm.ts @@ -0,0 +1,18 @@ +import { ExecutionContext, ExecutionResult } from '@flowgram.ai/runtime-interface'; + +import { LLMExecutor, LLMExecutorInputs } from '@nodes/llm'; +import { delay } from '@infra/utils'; + +export class MockLLMExecutor extends LLMExecutor { + public async execute(context: ExecutionContext): Promise { + const inputs = context.inputs as LLMExecutorInputs; + this.checkInputs(inputs); + await delay(100); // TODO mock node run + const result = `Hi, I'm an AI assistant, my name is ${inputs.modelName}, temperature is ${inputs.temperature}, system prompt is "${inputs.systemPrompt}", prompt is "${inputs.prompt}"`; + return { + outputs: { + result, + }, + }; + } +} diff --git a/packages/runtime/js-core/src/domain/__tests__/schemas/basic-llm.test.ts b/packages/runtime/js-core/src/domain/__tests__/schemas/basic-llm.test.ts new file mode 100644 index 00000000..b552e323 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/schemas/basic-llm.test.ts @@ -0,0 +1,78 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { IContainer, IEngine, IExecutor, WorkflowStatus } from '@flowgram.ai/runtime-interface'; + +import { LLMExecutor } from '@nodes/llm'; +import { snapshotsToVOData } from '../utils'; +import { WorkflowRuntimeContainer } from '../../container'; +import { TestSchemas } from '.'; + +let container: IContainer; + +beforeEach(() => { + container = WorkflowRuntimeContainer.instance; + const executor = container.get(IExecutor); + executor.register(new LLMExecutor()); +}); + +describe('workflow runtime basic test', () => { + it('should execute workflow', async () => { + if (process.env.ENABLE_MODEL_TEST !== 'true') { + return; + } + if (!process.env.MODEL_NAME || !process.env.API_KEY || !process.env.API_HOST) { + throw new Error('Missing environment variables'); + } + const engine = container.get(IEngine); + const modelName = process.env.MODEL_NAME; + const apiKey = process.env.API_KEY; + const apiHost = process.env.API_HOST; + const { context, processing } = engine.invoke({ + schema: TestSchemas.basicLLMSchema, + inputs: { + model_name: modelName, + api_key: apiKey, + api_host: apiHost, + prompt: 'Just give me the answer of "1+1=?", just one number, no other words', + }, + }); + expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing); + const result = await processing; + expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded); + expect(result).toStrictEqual({ + answer: '2', + }); + const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll()); + expect(snapshots).toStrictEqual([ + { + nodeID: 'start_0', + inputs: {}, + outputs: { + model_name: modelName, + api_key: apiKey, + api_host: apiHost, + prompt: 'Just give me the answer of "1+1=?", just one number, no other words', + }, + data: {}, + }, + { + nodeID: 'llm_0', + inputs: { + modelName: modelName, + apiKey: apiKey, + apiHost: apiHost, + temperature: 0, + prompt: 'Just give me the answer of "1+1=?", just one number, no other words', + systemPrompt: 'You are a helpful AI assistant.', + }, + outputs: { result: '2' }, + data: {}, + }, + { + nodeID: 'end_0', + inputs: { answer: '2' }, + outputs: { answer: '2' }, + data: {}, + }, + ]); + }); +}); diff --git a/packages/runtime/js-core/src/domain/__tests__/schemas/basic-llm.ts b/packages/runtime/js-core/src/domain/__tests__/schemas/basic-llm.ts new file mode 100644 index 00000000..876a7ec9 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/schemas/basic-llm.ts @@ -0,0 +1,169 @@ +import type { WorkflowSchema } from '@flowgram.ai/runtime-interface'; + +export const basicLLMSchema: WorkflowSchema = { + nodes: [ + { + id: 'start_0', + type: 'start', + meta: { + position: { + x: 0, + y: 0, + }, + }, + data: { + title: 'Start', + outputs: { + type: 'object', + properties: { + model_name: { + key: 14, + name: 'model_name', + type: 'string', + extra: { + index: 1, + }, + isPropertyRequired: true, + }, + prompt: { + key: 5, + name: 'prompt', + type: 'string', + extra: { + index: 3, + }, + isPropertyRequired: true, + }, + api_key: { + key: 19, + name: 'api_key', + type: 'string', + extra: { + index: 4, + }, + isPropertyRequired: true, + }, + api_host: { + key: 20, + name: 'api_host', + type: 'string', + extra: { + index: 5, + }, + isPropertyRequired: true, + }, + }, + required: ['model_name', 'prompt', 'api_key', 'api_host'], + }, + }, + }, + { + id: 'end_0', + type: 'end', + meta: { + position: { + x: 1000, + y: 0, + }, + }, + data: { + title: 'End', + inputsValues: { + answer: { + type: 'ref', + content: ['llm_0', 'result'], + }, + }, + inputs: { + type: 'object', + properties: { + answer: { + type: 'string', + }, + }, + }, + }, + }, + { + id: 'llm_0', + type: 'llm', + meta: { + position: { + x: 500, + y: 0, + }, + }, + data: { + title: 'LLM_0', + inputsValues: { + modelName: { + type: 'ref', + content: ['start_0', 'model_name'], + }, + apiKey: { + type: 'ref', + content: ['start_0', 'api_key'], + }, + apiHost: { + type: 'ref', + content: ['start_0', 'api_host'], + }, + temperature: { + type: 'constant', + content: 0, + }, + prompt: { + type: 'ref', + content: ['start_0', 'prompt'], + }, + systemPrompt: { + type: 'constant', + content: 'You are a helpful AI assistant.', + }, + }, + inputs: { + type: 'object', + required: ['modelName', 'temperature', 'prompt'], + properties: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { + type: 'string', + }, + temperature: { + type: 'number', + }, + systemPrompt: { + type: 'string', + }, + prompt: { + type: 'string', + }, + }, + }, + outputs: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + }, + }, + }, + ], + edges: [ + { + sourceNodeID: 'start_0', + targetNodeID: 'llm_0', + }, + { + sourceNodeID: 'llm_0', + targetNodeID: 'end_0', + }, + ], +}; diff --git a/packages/runtime/js-core/src/domain/__tests__/schemas/basic.test.ts b/packages/runtime/js-core/src/domain/__tests__/schemas/basic.test.ts new file mode 100644 index 00000000..69b1b894 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/schemas/basic.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest'; +import { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface'; + +import { snapshotsToVOData } from '../utils'; +import { WorkflowRuntimeContainer } from '../../container'; +import { TestSchemas } from '.'; + +const container: IContainer = WorkflowRuntimeContainer.instance; + +describe('WorkflowRuntime basic schema', () => { + it('should execute a workflow with input', async () => { + const engine = container.get(IEngine); + const { context, processing } = engine.invoke({ + schema: TestSchemas.basicSchema, + inputs: { + model_name: 'ai-model', + llm_settings: { + temperature: 0.5, + }, + prompt: 'How are you?', + }, + }); + expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing); + const result = await processing; + expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded); + expect(result).toStrictEqual({ + llm_res: `Hi, I'm an AI assistant, my name is ai-model, temperature is 0.5, system prompt is "You are a helpful AI assistant.", prompt is "How are you?"`, + llm_prompt: 'How are you?', + }); + const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll()); + expect(snapshots).toStrictEqual([ + { + nodeID: 'start_0', + inputs: {}, + outputs: { + model_name: 'ai-model', + llm_settings: { temperature: 0.5 }, + prompt: 'How are you?', + }, + data: {}, + }, + { + nodeID: 'llm_0', + inputs: { + modelName: 'ai-model', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.5, + prompt: 'How are you?', + systemPrompt: 'You are a helpful AI assistant.', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is ai-model, temperature is 0.5, system prompt is "You are a helpful AI assistant.", prompt is "How are you?"', + }, + data: {}, + }, + { + nodeID: 'end_0', + inputs: { + llm_res: + 'Hi, I\'m an AI assistant, my name is ai-model, temperature is 0.5, system prompt is "You are a helpful AI assistant.", prompt is "How are you?"', + llm_prompt: 'How are you?', + }, + outputs: { + llm_res: + 'Hi, I\'m an AI assistant, my name is ai-model, temperature is 0.5, system prompt is "You are a helpful AI assistant.", prompt is "How are you?"', + llm_prompt: 'How are you?', + }, + data: {}, + }, + ]); + const report = context.reporter.export(); + expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.llm_0.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded); + }); +}); diff --git a/packages/runtime/js-core/src/domain/__tests__/schemas/basic.ts b/packages/runtime/js-core/src/domain/__tests__/schemas/basic.ts new file mode 100644 index 00000000..a79b596d --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/schemas/basic.ts @@ -0,0 +1,177 @@ +import type { WorkflowSchema } from '@flowgram.ai/runtime-interface'; + +export const basicSchema: WorkflowSchema = { + nodes: [ + { + id: 'start_0', + type: 'start', + meta: { + position: { + x: 180, + y: 69, + }, + }, + data: { + title: 'Start', + outputs: { + type: 'object', + properties: { + model_name: { + key: 14, + name: 'model_name', + type: 'string', + extra: { + index: 1, + }, + isPropertyRequired: true, + }, + llm_settings: { + key: 17, + name: 'llm_settings', + type: 'object', + extra: { + index: 2, + }, + properties: { + temperature: { + key: 18, + name: 'temperature', + type: 'number', + extra: { + index: 1, + }, + }, + }, + required: [], + }, + prompt: { + key: 19, + name: 'prompt', + type: 'string', + extra: { + index: 3, + }, + isPropertyRequired: true, + }, + }, + required: ['model_name', 'prompt'], + }, + }, + }, + { + id: 'end_0', + type: 'end', + meta: { + position: { + x: 1121.3, + y: 69, + }, + }, + data: { + title: 'End', + inputsValues: { + llm_res: { + type: 'ref', + content: ['llm_0', 'result'], + }, + llm_prompt: { + type: 'ref', + content: ['start_0', 'prompt'], + }, + }, + inputs: { + type: 'object', + properties: { + llm_res: { + type: 'string', + }, + llm_prompt: { + type: 'string', + }, + }, + }, + }, + }, + { + id: 'llm_0', + type: 'llm', + meta: { + position: { + x: 650.65, + y: 0, + }, + }, + data: { + title: 'LLM_1', + inputsValues: { + modelName: { + type: 'ref', + content: ['start_0', 'model_name'], + }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, + temperature: { + type: 'ref', + content: ['start_0', 'llm_settings', 'temperature'], + }, + prompt: { + type: 'ref', + content: ['start_0', 'prompt'], + }, + systemPrompt: { + type: 'constant', + content: 'You are a helpful AI assistant.', + }, + }, + inputs: { + type: 'object', + required: ['modelName', 'temperature', 'prompt'], + properties: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { + type: 'string', + }, + temperature: { + type: 'number', + }, + systemPrompt: { + type: 'string', + }, + prompt: { + type: 'string', + }, + }, + }, + outputs: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + }, + }, + }, + ], + edges: [ + { + sourceNodeID: 'start_0', + targetNodeID: 'llm_0', + }, + { + sourceNodeID: 'llm_0', + targetNodeID: 'end_0', + }, + ], +}; diff --git a/packages/runtime/js-core/src/domain/__tests__/schemas/branch.test.ts b/packages/runtime/js-core/src/domain/__tests__/schemas/branch.test.ts new file mode 100644 index 00000000..70c49d8d --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/schemas/branch.test.ts @@ -0,0 +1,184 @@ +import { describe, expect, it } from 'vitest'; +import { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface'; + +import { snapshotsToVOData } from '../utils'; +import { WorkflowRuntimeContainer } from '../../container'; +import { TestSchemas } from '.'; + +const container: IContainer = WorkflowRuntimeContainer.instance; + +describe('WorkflowRuntime branch schema', () => { + it('should execute a workflow with branch 1', async () => { + const engine = container.get(IEngine); + const { context, processing } = engine.invoke({ + schema: TestSchemas.branchSchema, + inputs: { + model_id: 1, + prompt: 'Tell me a joke', + }, + }); + expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing); + const result = await processing; + expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded); + expect(result).toStrictEqual({ + m1_res: `Hi, I'm an AI assistant, my name is AI_MODEL_1, temperature is 0.5, system prompt is "I'm Model 1.", prompt is "Tell me a joke"`, + }); + const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll()); + expect(snapshots).toStrictEqual([ + { + nodeID: 'start_0', + inputs: {}, + outputs: { model_id: 1, prompt: 'Tell me a joke' }, + data: {}, + }, + { + nodeID: 'condition_0', + inputs: {}, + outputs: {}, + data: { + conditions: [ + { + value: { + left: { type: 'ref', content: ['start_0', 'model_id'] }, + operator: 'eq', + right: { type: 'constant', content: 1 }, + }, + key: 'if_1', + }, + { + value: { + left: { type: 'ref', content: ['start_0', 'model_id'] }, + operator: 'eq', + right: { type: 'constant', content: 2 }, + }, + key: 'if_2', + }, + ], + }, + branch: 'if_1', + }, + { + nodeID: 'llm_1', + inputs: { + modelName: 'AI_MODEL_1', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.5, + systemPrompt: "I'm Model 1.", + prompt: 'Tell me a joke', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.5, system prompt is "I\'m Model 1.", prompt is "Tell me a joke"', + }, + data: {}, + }, + { + nodeID: 'end_0', + inputs: { + m1_res: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.5, system prompt is "I\'m Model 1.", prompt is "Tell me a joke"', + }, + outputs: { + m1_res: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.5, system prompt is "I\'m Model 1.", prompt is "Tell me a joke"', + }, + data: {}, + }, + ]); + + const report = context.reporter.export(); + expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.condition_0.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.llm_1.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded); + }); + + it('should execute a workflow with branch 2', async () => { + const engine = container.get(IEngine); + const { context, processing } = engine.invoke({ + schema: TestSchemas.branchSchema, + inputs: { + model_id: 2, + prompt: 'Tell me a story', + }, + }); + expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing); + const result = await processing; + expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded); + expect(result).toStrictEqual({ + m2_res: `Hi, I'm an AI assistant, my name is AI_MODEL_2, temperature is 0.6, system prompt is "I'm Model 2.", prompt is "Tell me a story"`, + }); + const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll()); + expect(snapshots).toStrictEqual([ + { + nodeID: 'start_0', + inputs: {}, + outputs: { model_id: 2, prompt: 'Tell me a story' }, + data: {}, + }, + { + nodeID: 'condition_0', + inputs: {}, + outputs: {}, + data: { + conditions: [ + { + value: { + left: { type: 'ref', content: ['start_0', 'model_id'] }, + operator: 'eq', + right: { type: 'constant', content: 1 }, + }, + key: 'if_1', + }, + { + value: { + left: { type: 'ref', content: ['start_0', 'model_id'] }, + operator: 'eq', + right: { type: 'constant', content: 2 }, + }, + key: 'if_2', + }, + ], + }, + branch: 'if_2', + }, + { + nodeID: 'llm_2', + inputs: { + modelName: 'AI_MODEL_2', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.6, + systemPrompt: "I'm Model 2.", + prompt: 'Tell me a story', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_2, temperature is 0.6, system prompt is "I\'m Model 2.", prompt is "Tell me a story"', + }, + data: {}, + }, + { + nodeID: 'end_0', + inputs: { + m2_res: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_2, temperature is 0.6, system prompt is "I\'m Model 2.", prompt is "Tell me a story"', + }, + outputs: { + m2_res: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_2, temperature is 0.6, system prompt is "I\'m Model 2.", prompt is "Tell me a story"', + }, + data: {}, + }, + ]); + + const report = context.reporter.export(); + expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.condition_0.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.llm_2.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded); + }); +}); diff --git a/packages/runtime/js-core/src/domain/__tests__/schemas/branch.ts b/packages/runtime/js-core/src/domain/__tests__/schemas/branch.ts new file mode 100644 index 00000000..75a5f636 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/schemas/branch.ts @@ -0,0 +1,287 @@ +import { WorkflowSchema } from '@flowgram.ai/runtime-interface'; + +export const branchSchema: WorkflowSchema = { + nodes: [ + { + id: 'start_0', + type: 'start', + meta: { + position: { + x: 0, + y: 0, + }, + }, + data: { + title: 'Start', + outputs: { + type: 'object', + properties: { + model_id: { + key: 0, + name: 'model_id', + isPropertyRequired: false, + type: 'integer', + default: 'Hello Flow.', + extra: { + index: 0, + }, + }, + prompt: { + key: 5, + name: 'prompt', + isPropertyRequired: false, + type: 'string', + extra: { + index: 1, + }, + }, + }, + required: [], + }, + }, + }, + { + id: 'end_0', + type: 'end', + meta: { + position: { + x: 1500, + y: 0, + }, + }, + data: { + title: 'End', + inputs: { + type: 'object', + properties: { + m1_res: { + type: 'string', + }, + m2_res: { + type: 'string', + }, + }, + }, + inputsValues: { + m1_res: { + type: 'ref', + content: ['llm_1', 'result'], + }, + m2_res: { + type: 'ref', + content: ['llm_2', 'result'], + }, + }, + }, + }, + { + id: 'condition_0', + type: 'condition', + meta: { + position: { + x: 500, + y: 0, + }, + }, + data: { + title: 'Condition', + conditions: [ + { + value: { + left: { + type: 'ref', + content: ['start_0', 'model_id'], + }, + operator: 'eq', + right: { + type: 'constant', + content: 1, + }, + }, + key: 'if_1', + }, + { + value: { + left: { + type: 'ref', + content: ['start_0', 'model_id'], + }, + operator: 'eq', + right: { + type: 'constant', + content: 2, + }, + }, + key: 'if_2', + }, + ], + }, + }, + { + id: 'llm_1', + type: 'llm', + meta: { + position: { + x: 1000, + y: -500, + }, + }, + data: { + title: 'LLM_1', + inputsValues: { + modelName: { + type: 'constant', + content: 'AI_MODEL_1', + }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, + temperature: { + type: 'constant', + content: 0.5, + }, + systemPrompt: { + type: 'constant', + content: "I'm Model 1.", + }, + prompt: { + type: 'ref', + content: ['start_0', 'prompt'], + }, + }, + inputs: { + type: 'object', + required: ['modelName', 'temperature', 'prompt'], + properties: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { + type: 'string', + }, + temperature: { + type: 'number', + }, + systemPrompt: { + type: 'string', + }, + prompt: { + type: 'string', + }, + }, + }, + outputs: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + }, + }, + }, + { + id: 'llm_2', + type: 'llm', + meta: { + position: { + x: 1000, + y: 500, + }, + }, + data: { + title: 'LLM_2', + inputsValues: { + modelName: { + type: 'constant', + content: 'AI_MODEL_2', + }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, + temperature: { + type: 'constant', + content: 0.6, + }, + systemPrompt: { + type: 'constant', + content: "I'm Model 2.", + }, + prompt: { + type: 'ref', + content: ['start_0', 'prompt'], + }, + }, + inputs: { + type: 'object', + required: ['modelName', 'temperature', 'prompt'], + properties: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { + type: 'string', + }, + temperature: { + type: 'number', + }, + systemPrompt: { + type: 'string', + }, + prompt: { + type: 'string', + }, + }, + }, + outputs: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + }, + }, + }, + ], + edges: [ + { + sourceNodeID: 'start_0', + targetNodeID: 'condition_0', + }, + { + sourceNodeID: 'llm_1', + targetNodeID: 'end_0', + }, + { + sourceNodeID: 'llm_2', + targetNodeID: 'end_0', + }, + { + sourceNodeID: 'condition_0', + targetNodeID: 'llm_1', + sourcePortID: 'if_1', + }, + { + sourceNodeID: 'condition_0', + targetNodeID: 'llm_2', + sourcePortID: 'if_2', + }, + ], +}; diff --git a/packages/runtime/js-core/src/domain/__tests__/schemas/index.ts b/packages/runtime/js-core/src/domain/__tests__/schemas/index.ts new file mode 100644 index 00000000..473e4809 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/schemas/index.ts @@ -0,0 +1,13 @@ +import { twoLLMSchema } from './two-llm'; +import { loopSchema } from './loop'; +import { branchSchema } from './branch'; +import { basicLLMSchema } from './basic-llm'; +import { basicSchema } from './basic'; + +export const TestSchemas = { + twoLLMSchema, + basicSchema, + branchSchema, + basicLLMSchema, + loopSchema, +}; diff --git a/packages/runtime/js-core/src/domain/__tests__/schemas/loop.test.ts b/packages/runtime/js-core/src/domain/__tests__/schemas/loop.test.ts new file mode 100644 index 00000000..07ab10c4 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/schemas/loop.test.ts @@ -0,0 +1,189 @@ +import { describe, expect, it } from 'vitest'; +import { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface'; + +import { snapshotsToVOData } from '../utils'; +import { WorkflowRuntimeContainer } from '../../container'; +import { TestSchemas } from '.'; + +const container: IContainer = WorkflowRuntimeContainer.instance; + +describe('WorkflowRuntime loop schema', () => { + it('should execute a workflow with input', async () => { + const engine = container.get(IEngine); + const { context, processing } = engine.invoke({ + schema: TestSchemas.loopSchema, + inputs: { + prompt: 'How are you?', + tasks: [ + 'TASK - A', + 'TASK - B', + 'TASK - C', + 'TASK - D', + 'TASK - E', + 'TASK - F', + 'TASK - G', + 'TASK - H', + ], + }, + }); + expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing); + const result = await processing; + expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded); + expect(result).toStrictEqual({}); + const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll()); + expect(snapshots).toStrictEqual([ + { + nodeID: 'start_0', + inputs: {}, + outputs: { + prompt: 'How are you?', + tasks: [ + 'TASK - A', + 'TASK - B', + 'TASK - C', + 'TASK - D', + 'TASK - E', + 'TASK - F', + 'TASK - G', + 'TASK - H', + ], + }, + data: {}, + }, + { + nodeID: 'loop_0', + inputs: {}, + outputs: {}, + data: { batchFor: { type: 'ref', content: ['start_0', 'tasks'] } }, + }, + { + nodeID: 'llm_0', + inputs: { + modelName: 'AI_MODEL_1', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.6, + prompt: 'TASK - A', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.6, system prompt is "undefined", prompt is "TASK - A"', + }, + data: {}, + }, + { + nodeID: 'llm_0', + inputs: { + modelName: 'AI_MODEL_1', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.6, + prompt: 'TASK - B', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.6, system prompt is "undefined", prompt is "TASK - B"', + }, + data: {}, + }, + { + nodeID: 'llm_0', + inputs: { + modelName: 'AI_MODEL_1', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.6, + prompt: 'TASK - C', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.6, system prompt is "undefined", prompt is "TASK - C"', + }, + data: {}, + }, + { + nodeID: 'llm_0', + inputs: { + modelName: 'AI_MODEL_1', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.6, + prompt: 'TASK - D', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.6, system prompt is "undefined", prompt is "TASK - D"', + }, + data: {}, + }, + { + nodeID: 'llm_0', + inputs: { + modelName: 'AI_MODEL_1', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.6, + prompt: 'TASK - E', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.6, system prompt is "undefined", prompt is "TASK - E"', + }, + data: {}, + }, + { + nodeID: 'llm_0', + inputs: { + modelName: 'AI_MODEL_1', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.6, + prompt: 'TASK - F', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.6, system prompt is "undefined", prompt is "TASK - F"', + }, + data: {}, + }, + { + nodeID: 'llm_0', + inputs: { + modelName: 'AI_MODEL_1', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.6, + prompt: 'TASK - G', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.6, system prompt is "undefined", prompt is "TASK - G"', + }, + data: {}, + }, + { + nodeID: 'llm_0', + inputs: { + modelName: 'AI_MODEL_1', + apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + apiHost: 'https://mock-ai-url/api/v3', + temperature: 0.6, + prompt: 'TASK - H', + }, + outputs: { + result: + 'Hi, I\'m an AI assistant, my name is AI_MODEL_1, temperature is 0.6, system prompt is "undefined", prompt is "TASK - H"', + }, + data: {}, + }, + { nodeID: 'end_0', inputs: {}, outputs: {}, data: {} }, + ]); + const report = context.reporter.export(); + expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.loop_0.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.llm_0.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded); + expect(report.reports.llm_0.snapshots.length).toBe(8); + }); +}); diff --git a/packages/runtime/js-core/src/domain/__tests__/schemas/loop.ts b/packages/runtime/js-core/src/domain/__tests__/schemas/loop.ts new file mode 100644 index 00000000..bd16ea8a --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/schemas/loop.ts @@ -0,0 +1,164 @@ +import { WorkflowSchema } from '@flowgram.ai/runtime-interface'; + +export const loopSchema: WorkflowSchema = { + nodes: [ + { + id: 'start_0', + type: 'start', + meta: { + position: { + x: 180, + y: 218.5, + }, + }, + data: { + title: 'Start', + outputs: { + type: 'object', + properties: { + tasks: { + key: 7, + name: 'tasks', + isPropertyRequired: true, + type: 'array', + extra: { + index: 0, + }, + items: { + type: 'string', + }, + }, + system_prompt: { + key: 1, + name: 'system_prompt', + isPropertyRequired: true, + type: 'string', + extra: { + index: 1, + }, + }, + }, + required: ['tasks', 'system_prompt'], + }, + }, + }, + { + id: 'end_0', + type: 'end', + meta: { + position: { + x: 1340, + y: 228.5, + }, + }, + data: { + title: 'End', + inputs: { + type: 'object', + properties: {}, + }, + inputsValues: {}, + }, + }, + { + id: 'loop_0', + type: 'loop', + meta: { + position: { + x: 560, + y: 125, + }, + }, + data: { + title: 'Loop_1', + batchFor: { + type: 'ref', + content: ['start_0', 'tasks'], + }, + }, + blocks: [ + { + id: 'llm_0', + type: 'llm', + meta: { + position: { + x: 200, + y: 0, + }, + }, + data: { + title: 'LLM_1', + inputsValues: { + modelName: { + type: 'constant', + content: 'AI_MODEL_1', + }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, + temperature: { + type: 'constant', + content: 0.6, + }, + systemPrompt: { + type: 'ref', + content: ['start_0', 'system_prompt'], + }, + prompt: { + type: 'ref', + content: ['loop_0_locals', 'item'], + }, + }, + inputs: { + type: 'object', + required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'], + properties: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { + type: 'string', + }, + temperature: { + type: 'number', + }, + systemPrompt: { + type: 'string', + }, + prompt: { + type: 'string', + }, + }, + }, + outputs: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + }, + }, + }, + ], + }, + ], + edges: [ + { + sourceNodeID: 'start_0', + targetNodeID: 'loop_0', + }, + { + sourceNodeID: 'loop_0', + targetNodeID: 'end_0', + }, + ], +}; diff --git a/packages/runtime/js-core/src/domain/__tests__/schemas/two-llm.ts b/packages/runtime/js-core/src/domain/__tests__/schemas/two-llm.ts new file mode 100644 index 00000000..f0bd6e17 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/schemas/two-llm.ts @@ -0,0 +1,267 @@ +import { WorkflowSchema } from '@flowgram.ai/runtime-interface'; + +export const twoLLMSchema: WorkflowSchema = { + nodes: [ + { + id: 'start_0', + type: 'start', + meta: { + position: { + x: 180, + y: 222.5, + }, + }, + data: { + title: 'Start', + outputs: { + type: 'object', + properties: { + query: { + key: 5, + name: 'query', + isPropertyRequired: false, + type: 'string', + default: 'Hello Flow.', + extra: { + index: 0, + }, + }, + enable: { + key: 6, + name: 'enable', + isPropertyRequired: false, + type: 'boolean', + default: true, + extra: { + index: 1, + }, + }, + array_obj: { + key: 7, + name: 'array_obj', + isPropertyRequired: false, + type: 'array', + items: { + type: 'object', + properties: { + int: { + type: 'number', + }, + str: { + type: 'string', + }, + }, + }, + extra: { + index: 2, + }, + }, + num: { + key: 10, + name: 'num', + isPropertyRequired: false, + type: 'number', + extra: { + index: 3, + }, + }, + model: { + key: 24, + name: 'model', + type: 'string', + extra: { + index: 5, + }, + }, + }, + required: [], + }, + }, + }, + { + id: 'end_0', + type: 'end', + meta: { + position: { + x: 1100, + y: 235.5, + }, + }, + data: { + title: 'End', + outputs: { + type: 'object', + properties: { + result: { + type: 'string', + default: { + type: 'ref', + content: ['llm_BjEpK', 'result'], + }, + }, + }, + }, + }, + }, + { + id: 'llm_2', + type: 'llm', + meta: { + position: { + x: 640, + y: 327, + }, + }, + data: { + title: 'LLM_2', + inputsValues: { + systemPrompt: { + type: 'constant', + content: 'BBBB', + }, + modelName: { + type: 'ref', + content: ['start_0', 'model'], + }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, + temperature: { + type: 'ref', + content: ['start_0', 'num'], + }, + prompt: { + type: 'ref', + content: ['start_0', 'query'], + }, + }, + inputs: { + type: 'object', + required: ['modelName', 'temperature', 'prompt'], + properties: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { + type: 'string', + }, + temperature: { + type: 'number', + }, + systemPrompt: { + type: 'string', + }, + prompt: { + type: 'string', + }, + }, + }, + outputs: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + }, + }, + }, + { + id: 'llm_1', + type: 'llm', + meta: { + position: { + x: 640, + y: 0, + }, + }, + data: { + title: 'LLM_1', + inputsValues: { + modelName: { + type: 'ref', + content: ['start_0', 'model'], + }, + apiKey: { + type: 'constant', + content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + apiHost: { + type: 'constant', + content: 'https://mock-ai-url/api/v3', + }, + temperature: { + type: 'ref', + content: ['start_0', 'num'], + }, + systemPrompt: { + type: 'constant', + content: 'AAAA', + }, + prompt: { + type: 'ref', + content: ['start_0', 'query'], + }, + }, + inputs: { + type: 'object', + required: ['modelName', 'temperature', 'prompt'], + properties: { + modelName: { + type: 'string', + }, + apiKey: { + type: 'string', + }, + apiHost: { + type: 'string', + }, + temperature: { + type: 'number', + }, + systemPrompt: { + type: 'string', + }, + prompt: { + type: 'string', + }, + }, + }, + outputs: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + }, + }, + }, + ], + edges: [ + { + sourceNodeID: 'start_0', + targetNodeID: 'llm_1', + }, + { + sourceNodeID: 'start_0', + targetNodeID: 'llm_2', + }, + { + sourceNodeID: 'llm_2', + targetNodeID: 'end_0', + }, + { + sourceNodeID: 'llm_1', + targetNodeID: 'end_0', + }, + ], +}; diff --git a/packages/runtime/js-core/src/domain/__tests__/setup.ts b/packages/runtime/js-core/src/domain/__tests__/setup.ts new file mode 100644 index 00000000..ad0b7168 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/setup.ts @@ -0,0 +1,8 @@ +import { IExecutor } from '@flowgram.ai/runtime-interface'; + +import { MockLLMExecutor } from './executor/llm'; +import { WorkflowRuntimeContainer } from '../container'; + +const container = WorkflowRuntimeContainer.instance; +const executor = container.get(IExecutor); +executor.register(new MockLLMExecutor()); diff --git a/packages/runtime/js-core/src/domain/__tests__/utils/index.ts b/packages/runtime/js-core/src/domain/__tests__/utils/index.ts new file mode 100644 index 00000000..85797488 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/utils/index.ts @@ -0,0 +1 @@ +export { snapshotsToVOData } from './snapshot'; diff --git a/packages/runtime/js-core/src/domain/__tests__/utils/snapshot.ts b/packages/runtime/js-core/src/domain/__tests__/utils/snapshot.ts new file mode 100644 index 00000000..75ac2f33 --- /dev/null +++ b/packages/runtime/js-core/src/domain/__tests__/utils/snapshot.ts @@ -0,0 +1,16 @@ +import { Snapshot, VOData } from '@flowgram.ai/runtime-interface'; + +export const snapshotsToVOData = (snapshots: Snapshot[]): VOData[] => + snapshots.map((snapshot) => { + const { nodeID, inputs, outputs, data, branch } = snapshot; + const newSnapshot: VOData = { + nodeID, + inputs, + outputs, + data, + }; + if (branch) { + newSnapshot.branch = branch; + } + return newSnapshot; + }); diff --git a/packages/runtime/js-core/src/domain/container/index.test.ts b/packages/runtime/js-core/src/domain/container/index.test.ts new file mode 100644 index 00000000..79bd4857 --- /dev/null +++ b/packages/runtime/js-core/src/domain/container/index.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import { IEngine, IExecutor, IValidation } from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeContainer } from './index'; + +describe('WorkflowRuntimeContainer', () => { + it('should create a container instance', () => { + const container = new WorkflowRuntimeContainer({}); + expect(container).toBeDefined(); + expect(container).toBeInstanceOf(WorkflowRuntimeContainer); + }); + + it('should get services correctly', () => { + const mockServices = { + [IValidation]: { id: 'validation' }, + [IExecutor]: { id: 'executor' }, + [IEngine]: { id: 'engine' }, + }; + + const container = new WorkflowRuntimeContainer(mockServices); + + expect(container.get(IValidation)).toEqual({ id: 'validation' }); + expect(container.get(IExecutor)).toEqual({ id: 'executor' }); + expect(container.get(IEngine)).toEqual({ id: 'engine' }); + }); + + it('should maintain singleton instance', () => { + const instance1 = WorkflowRuntimeContainer.instance; + const instance2 = WorkflowRuntimeContainer.instance; + + expect(instance1).toBeDefined(); + expect(instance2).toBeDefined(); + expect(instance1).toBe(instance2); + }); + + it('should create services correctly', () => { + const services = (WorkflowRuntimeContainer as any).create(); + + expect(services[IValidation]).toBeDefined(); + expect(services[IExecutor]).toBeDefined(); + expect(services[IEngine]).toBeDefined(); + }); +}); diff --git a/packages/runtime/js-core/src/domain/container/index.ts b/packages/runtime/js-core/src/domain/container/index.ts new file mode 100644 index 00000000..91f2dc2f --- /dev/null +++ b/packages/runtime/js-core/src/domain/container/index.ts @@ -0,0 +1,46 @@ +import { + ContainerService, + IContainer, + IEngine, + IExecutor, + IValidation, +} from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeNodeExecutors } from '@nodes/index'; +import { WorkflowRuntimeValidation } from '../validation'; +import { WorkflowRuntimeExecutor } from '../executor'; +import { WorkflowRuntimeEngine } from '../engine'; + +export class WorkflowRuntimeContainer implements IContainer { + constructor(private readonly services: Record) {} + + public get(key: any): T { + return this.services[key] as T; + } + + private static _instance: IContainer; + + public static get instance(): IContainer { + if (this._instance) { + return this._instance; + } + const services = this.create(); + this._instance = new WorkflowRuntimeContainer(services); + return this._instance; + } + + private static create(): Record { + // services + const Validation = new WorkflowRuntimeValidation(); + const Executor = new WorkflowRuntimeExecutor(WorkflowRuntimeNodeExecutors); + const Engine = new WorkflowRuntimeEngine({ + Executor, + }); + + return { + [IValidation]: Validation, + [IExecutor]: Executor, + [IEngine]: Engine, + }; + } +} diff --git a/packages/runtime/js-core/src/domain/context/index.ts b/packages/runtime/js-core/src/domain/context/index.ts new file mode 100644 index 00000000..d084214b --- /dev/null +++ b/packages/runtime/js-core/src/domain/context/index.ts @@ -0,0 +1,116 @@ +import { + InvokeParams, + IContext, + IDocument, + IState, + ISnapshotCenter, + IVariableStore, + IStatusCenter, + IReporter, + IIOCenter, + ContextData, +} from '@flowgram.ai/runtime-interface'; + +import { uuid } from '@infra/utils'; +import { WorkflowRuntimeVariableStore } from '../variable'; +import { WorkflowRuntimeStatusCenter } from '../status'; +import { WorkflowRuntimeState } from '../state'; +import { WorkflowRuntimeSnapshotCenter } from '../snapshot'; +import { WorkflowRuntimeReporter } from '../report'; +import { WorkflowRuntimeIOCenter } from '../io-center'; +import { WorkflowRuntimeDocument } from '../document'; + +export class WorkflowRuntimeContext implements IContext { + public readonly id: string; + + public readonly document: IDocument; + + public readonly variableStore: IVariableStore; + + public readonly state: IState; + + public readonly ioCenter: IIOCenter; + + public readonly snapshotCenter: ISnapshotCenter; + + public readonly statusCenter: IStatusCenter; + + public readonly reporter: IReporter; + + private subContexts: IContext[] = []; + + constructor(data: ContextData) { + this.id = uuid(); + this.document = data.document; + this.variableStore = data.variableStore; + this.state = data.state; + this.ioCenter = data.ioCenter; + this.snapshotCenter = data.snapshotCenter; + this.statusCenter = data.statusCenter; + this.reporter = data.reporter; + } + + public init(params: InvokeParams): void { + const { schema, inputs } = params; + this.document.init(schema); + this.variableStore.init(); + this.state.init(); + this.ioCenter.init(inputs); + this.snapshotCenter.init(); + this.statusCenter.init(); + this.reporter.init(); + } + + public dispose(): void { + this.subContexts.forEach((subContext) => { + subContext.dispose(); + }); + this.subContexts = []; + this.document.dispose(); + this.variableStore.dispose(); + this.state.dispose(); + this.ioCenter.dispose(); + this.snapshotCenter.dispose(); + this.statusCenter.dispose(); + this.reporter.dispose(); + } + + public sub(): IContext { + const variableStore = new WorkflowRuntimeVariableStore(); + variableStore.setParent(this.variableStore); + const state = new WorkflowRuntimeState(variableStore); + const contextData: ContextData = { + document: this.document, + ioCenter: this.ioCenter, + snapshotCenter: this.snapshotCenter, + statusCenter: this.statusCenter, + reporter: this.reporter, + variableStore, + state, + }; + const subContext = new WorkflowRuntimeContext(contextData); + this.subContexts.push(subContext); + subContext.variableStore.init(); + subContext.state.init(); + return subContext; + } + + public static create(): IContext { + const document = new WorkflowRuntimeDocument(); + const variableStore = new WorkflowRuntimeVariableStore(); + const state = new WorkflowRuntimeState(variableStore); + const ioCenter = new WorkflowRuntimeIOCenter(); + const snapshotCenter = new WorkflowRuntimeSnapshotCenter(); + const statusCenter = new WorkflowRuntimeStatusCenter(); + const reporter = new WorkflowRuntimeReporter(ioCenter, snapshotCenter, statusCenter); + return new WorkflowRuntimeContext({ + document, + variableStore, + state, + ioCenter, + snapshotCenter, + statusCenter, + reporter, + }); + } +} diff --git a/packages/runtime/js-core/src/domain/document/document/create-store.test.ts b/packages/runtime/js-core/src/domain/document/document/create-store.test.ts new file mode 100644 index 00000000..b361faa0 --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/document/create-store.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from 'vitest'; +import { FlowGramNode, WorkflowPortType } from '@flowgram.ai/runtime-interface'; + +import { createStore } from './create-store'; + +describe('createStore', () => { + it('should create an empty store', () => { + const store = createStore({ + flattenSchema: { nodes: [], edges: [] }, + nodeBlocks: new Map(), + nodeEdges: new Map(), + }); + + expect(store.nodes.size).toBe(1); // 只有 root 节点 + expect(store.edges.size).toBe(0); + expect(store.ports.size).toBe(0); + + const rootNode = store.nodes.get(FlowGramNode.Root); + expect(rootNode).toBeDefined(); + expect(rootNode?.type).toBe(FlowGramNode.Root); + expect(rootNode?.position).toEqual({ x: 0, y: 0 }); + }); + + it('should create store with nodes and edges', () => { + const store = createStore({ + flattenSchema: { + nodes: [ + { + id: 'node1', + type: 'TestNode', + meta: { position: { x: 100, y: 100 } }, + data: { + title: 'Test Node 1', + inputsValues: { test: 'value' }, + inputs: ['input1'], + outputs: ['output1'], + }, + }, + { + id: 'node2', + type: 'TestNode', + meta: { position: { x: 200, y: 200 } }, + data: { + title: 'Test Node 2', + }, + }, + ], + edges: [ + { + sourceNodeID: 'node1', + targetNodeID: 'node2', + sourcePortID: 'output1', + targetPortID: 'input1', + }, + ], + }, + nodeBlocks: new Map([['root', ['node1', 'node2']]]), + nodeEdges: new Map([['root', ['node1-node2']]]), + }); + + // 验证节点创建 + expect(store.nodes.size).toBe(3); // root + 2个测试节点 + const node1 = store.nodes.get('node1'); + expect(node1?.type).toBe('TestNode'); + expect(node1?.name).toBe('Test Node 1'); + expect(node1?.position).toEqual({ x: 100, y: 100 }); + expect(node1?.declare).toEqual({ + inputsValues: { test: 'value' }, + inputs: ['input1'], + outputs: ['output1'], + }); + + // 验证边创建 + expect(store.edges.size).toBe(1); + const edge = Array.from(store.edges.values())[0]; + expect(edge.from).toBe(node1); + expect(edge.to).toBe(store.nodes.get('node2')); + + // 验证端口创建 + expect(store.ports.size).toBe(2); // 输入端口和输出端口 + const outputPort = store.ports.get('output1'); + const inputPort = store.ports.get('input1'); + expect(outputPort?.type).toBe(WorkflowPortType.Output); + expect(inputPort?.type).toBe(WorkflowPortType.Input); + + // 验证节点关系 + const rootNode = store.nodes.get(FlowGramNode.Root); + expect(rootNode?.children).toHaveLength(2); + expect(node1?.parent).toBe(rootNode); + }); + + it('should throw error for invalid edge', () => { + expect(() => + createStore({ + flattenSchema: { + nodes: [], + edges: [ + { + sourceNodeID: 'nonexistent1', + targetNodeID: 'nonexistent2', + }, + ], + }, + nodeBlocks: new Map(), + nodeEdges: new Map(), + }) + ).toThrow('invalid edge schema ID'); + }); +}); diff --git a/packages/runtime/js-core/src/domain/document/document/create-store.ts b/packages/runtime/js-core/src/domain/document/document/create-store.ts new file mode 100644 index 00000000..68d7f07b --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/document/create-store.ts @@ -0,0 +1,133 @@ +import { + FlowGramNode, + WorkflowPortType, + type CreateEdgeParams, + type CreateNodeParams, + type CreatePortParams, +} from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeEdge, WorkflowRuntimeNode, WorkflowRuntimePort } from '../entity'; +import { FlattenData } from './flat-schema'; + +export interface DocumentStore { + nodes: Map; + edges: Map; + ports: Map; +} + +const createNode = (store: DocumentStore, params: CreateNodeParams): WorkflowRuntimeNode => { + const node = new WorkflowRuntimeNode(params); + store.nodes.set(node.id, node); + return node; +}; + +const createEdge = (store: DocumentStore, params: CreateEdgeParams): WorkflowRuntimeEdge => { + const edge = new WorkflowRuntimeEdge(params); + store.edges.set(edge.id, edge); + return edge; +}; + +const getOrCreatePort = (store: DocumentStore, params: CreatePortParams): WorkflowRuntimePort => { + const createdPort = store.ports.get(params.id); + if (createdPort) { + return createdPort as WorkflowRuntimePort; + } + const port = new WorkflowRuntimePort(params); + store.ports.set(port.id, port); + return port; +}; + +export const createStore = (params: FlattenData): DocumentStore => { + const { flattenSchema, nodeBlocks } = params; + const { nodes, edges } = flattenSchema; + const store: DocumentStore = { + nodes: new Map(), + edges: new Map(), + ports: new Map(), + }; + // create root node + createNode(store, { + id: FlowGramNode.Root, + type: FlowGramNode.Root, + name: FlowGramNode.Root, + position: { x: 0, y: 0 }, + }); + // create nodes + nodes.forEach((nodeSchema) => { + const id = nodeSchema.id; + const type = nodeSchema.type as FlowGramNode; + const { + title = `${type}-${id}-untitled`, + inputsValues, + inputs, + outputs, + ...data + } = nodeSchema.data ?? {}; + createNode(store, { + id, + type, + name: title, + position: nodeSchema.meta.position, + variable: { inputsValues, inputs, outputs }, + data, + }); + }); + // create node relations + nodeBlocks.forEach((blockIDs, parentID) => { + const parent = store.nodes.get(parentID) as WorkflowRuntimeNode; + const children = blockIDs + .map((id) => store.nodes.get(id)) + .filter(Boolean) as WorkflowRuntimeNode[]; + children.forEach((child) => { + child.parent = parent; + parent.addChild(child); + }); + }); + // create edges & ports + edges.forEach((edgeSchema) => { + const id = WorkflowRuntimeEdge.createID(edgeSchema); + const { + sourceNodeID, + targetNodeID, + sourcePortID = 'defaultOutput', + targetPortID = 'defaultInput', + } = edgeSchema; + const from = store.nodes.get(sourceNodeID); + const to = store.nodes.get(targetNodeID); + if (!from || !to) { + throw new Error(`invalid edge schema ID: ${id}, from: ${sourceNodeID}, to: ${targetNodeID}`); + } + const edge = createEdge(store, { + id, + from, + to, + }); + + // create from port + const fromPort = getOrCreatePort(store, { + node: from, + id: sourcePortID, + type: WorkflowPortType.Output, + }); + + // build relation + fromPort.addEdge(edge); + edge.fromPort = fromPort; + from.addPort(fromPort); + from.addOutputEdge(edge); + + // create to port + const toPort = getOrCreatePort(store, { + node: to, + id: targetPortID, + type: WorkflowPortType.Input, + }); + + // build relation + toPort.addEdge(edge); + edge.toPort = toPort; + to.addPort(toPort); + to.addInputEdge(edge); + }); + return store; +}; diff --git a/packages/runtime/js-core/src/domain/document/document/flat-schema.test.ts b/packages/runtime/js-core/src/domain/document/document/flat-schema.test.ts new file mode 100644 index 00000000..1b235149 --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/document/flat-schema.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest'; +import { FlowGramNode, WorkflowSchema } from '@flowgram.ai/runtime-interface'; + +import { flatSchema } from './flat-schema'; + +describe('flatSchema', () => { + it('should handle empty schema', () => { + const result = flatSchema({}); + expect(result.flattenSchema.nodes).toEqual([]); + expect(result.flattenSchema.edges).toEqual([]); + expect(result.nodeBlocks.get(FlowGramNode.Root)).toEqual([]); + expect(result.nodeEdges.get(FlowGramNode.Root)).toEqual([]); + }); + + it('should handle basic schema without nested blocks', () => { + const schema = { + nodes: [ + { id: 'node1', type: 'test' }, + { id: 'node2', type: 'test' }, + ], + edges: [{ sourceNodeID: 'node1', targetNodeID: 'node2' }], + } as unknown as WorkflowSchema; + + const result = flatSchema(schema); + expect(result.flattenSchema.nodes).toEqual(schema.nodes); + expect(result.flattenSchema.edges).toEqual(schema.edges); + expect(result.nodeBlocks.get(FlowGramNode.Root)).toEqual(['node1', 'node2']); + expect(result.nodeEdges.get(FlowGramNode.Root)).toEqual(['node1-node2']); + }); + + it('should flatten nested blocks and edges', () => { + const schema = { + nodes: [ + { + id: 'parent', + type: 'container', + blocks: [ + { id: 'child1', type: 'test' }, + { + id: 'child2', + type: 'test', + blocks: [{ id: 'grandchild', type: 'test' }], + edges: [{ sourceNodeID: 'child2', targetNodeID: 'grandchild' }], + }, + ], + edges: [{ sourceNodeID: 'child1', targetNodeID: 'child2' }], + }, + ], + edges: [], + } as unknown as WorkflowSchema; + + const result = flatSchema(schema); + + // 验证节点被正确展平 + expect(result.flattenSchema.nodes.map((n) => n.id)).toEqual([ + 'parent', + 'child1', + 'child2', + 'grandchild', + ]); + + // 验证边被正确展平 + expect(result.flattenSchema.edges.length).toBe(2); + + // 验证节点关系映射 + expect(result.nodeBlocks.get('parent')).toEqual(['child1', 'child2']); + expect(result.nodeBlocks.get('child2')).toEqual(['grandchild']); + + // 验证边关系映射 + expect(result.nodeEdges.get('parent')).toEqual(['child1-child2']); + expect(result.nodeEdges.get('child2')).toEqual(['child2-grandchild']); + }); +}); diff --git a/packages/runtime/js-core/src/domain/document/document/flat-schema.ts b/packages/runtime/js-core/src/domain/document/document/flat-schema.ts new file mode 100644 index 00000000..48e84b34 --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/document/flat-schema.ts @@ -0,0 +1,73 @@ +import { WorkflowNodeSchema, WorkflowSchema, FlowGramNode } from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeEdge } from '../entity'; + +export interface FlattenData { + flattenSchema: WorkflowSchema; + nodeBlocks: Map; + nodeEdges: Map; +} + +type FlatSchema = (json: Partial) => FlattenData; + +const flatLayer = (data: FlattenData, nodeSchema: WorkflowNodeSchema) => { + const { blocks, edges } = nodeSchema; + if (blocks) { + data.flattenSchema.nodes.push(...blocks); + const blockIDs: string[] = []; + blocks.forEach((block) => { + blockIDs.push(block.id); + // 递归处理子节点的 blocks 和 edges + if (block.blocks) { + flatLayer(data, block); + } + }); + data.nodeBlocks.set(nodeSchema.id, blockIDs); + delete nodeSchema.blocks; + } + if (edges) { + data.flattenSchema.edges.push(...edges); + const edgeIDs: string[] = []; + edges.forEach((edge) => { + const edgeID = WorkflowRuntimeEdge.createID(edge); + edgeIDs.push(edgeID); + }); + data.nodeEdges.set(nodeSchema.id, edgeIDs); + delete nodeSchema.edges; + } +}; + +/** + * flat the tree json structure, extract the structure information to map + */ +export const flatSchema: FlatSchema = (schema = { nodes: [], edges: [] }) => { + const rootNodes = schema.nodes ?? []; + const rootEdges = schema.edges ?? []; + + const data: FlattenData = { + flattenSchema: { + nodes: [], + edges: [], + }, + nodeBlocks: new Map(), + nodeEdges: new Map(), + }; + + const root: WorkflowNodeSchema = { + id: FlowGramNode.Root, + type: FlowGramNode.Root, + blocks: rootNodes, + edges: rootEdges, + meta: { + position: { + x: 0, + y: 0, + }, + }, + data: {}, + }; + + flatLayer(data, root); + + return data; +}; diff --git a/packages/runtime/js-core/src/domain/document/document/index.test.ts b/packages/runtime/js-core/src/domain/document/document/index.test.ts new file mode 100644 index 00000000..240f4f8c --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/document/index.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from 'vitest'; + +import { TestSchemas } from '@workflow/__tests__/schemas'; +import { WorkflowRuntimeDocument } from './index'; + +describe('WorkflowRuntimeDocument create', () => { + it('should create instance', () => { + const document = new WorkflowRuntimeDocument(); + expect(document).toBeDefined(); + expect(document.id).toBeDefined(); + }); + + it('should has root', () => { + const schema = { + nodes: [], + edges: [], + }; + const document = new WorkflowRuntimeDocument(); + document.init(schema); + expect(document.root).toBeDefined(); + }); + + it('should init', () => { + const document = new WorkflowRuntimeDocument(); + document.init(TestSchemas.basicSchema); + const nodeIDs = document.nodes.map((n) => n.id); + const edgeIDs = document.edges.map((e) => e.id); + expect(nodeIDs).toEqual(['root', 'start_0', 'end_0', 'llm_0']); + expect(edgeIDs).toEqual(['start_0-llm_0', 'llm_0-end_0']); + }); + + it('should dispose', () => { + const document = new WorkflowRuntimeDocument(); + document.init(TestSchemas.basicSchema); + expect(document.nodes.length).toBe(4); + expect(document.edges.length).toBe(2); + document.dispose(); + expect(document.nodes.length).toBe(0); + expect(document.edges.length).toBe(0); + }); + + it('should has start & end', () => { + const document = new WorkflowRuntimeDocument(); + document.init(TestSchemas.basicSchema); + expect(document.start.id).toBe('start_0'); + expect(document.end.id).toBe('end_0'); + }); + + it('should get node by id', () => { + const document = new WorkflowRuntimeDocument(); + document.init(TestSchemas.basicSchema); + + const node = document.getNode('llm_0'); + expect(node).toBeDefined(); + expect(node?.id).toBe('llm_0'); + expect(node?.type).toBe('llm'); + + const nonExistNode = document.getNode('non_exist'); + expect(nonExistNode).toBeNull(); + }); + + it('should get edge by id', () => { + const document = new WorkflowRuntimeDocument(); + document.init(TestSchemas.basicSchema); + + const edge = document.getEdge('start_0-llm_0'); + expect(edge).toBeDefined(); + expect(edge?.id).toBe('start_0-llm_0'); + + const nonExistEdge = document.getEdge('non_exist'); + expect(nonExistEdge).toBeNull(); + }); + + it('should init with two LLM schema', () => { + const document = new WorkflowRuntimeDocument(); + document.init(TestSchemas.twoLLMSchema); + + expect(document.nodes.length).toBeGreaterThan(4); // 包含 root, start, end 和至少两个 LLM 节点 + expect(document.edges.length).toBeGreaterThan(2); // 至少有 3 条边连接这些节点 + + // 验证所有必需的节点都存在 + expect(document.root).toBeDefined(); + expect(document.start).toBeDefined(); + expect(document.end).toBeDefined(); + }); +}); diff --git a/packages/runtime/js-core/src/domain/document/document/index.ts b/packages/runtime/js-core/src/domain/document/document/index.ts new file mode 100644 index 00000000..d8efb48a --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/document/index.ts @@ -0,0 +1,72 @@ +import { + type WorkflowSchema, + FlowGramNode, + type IDocument, + type IEdge, + type INode, +} from '@flowgram.ai/runtime-interface'; + +import { uuid } from '@infra/utils'; +import { flatSchema } from './flat-schema'; +import { createStore, DocumentStore } from './create-store'; + +export class WorkflowRuntimeDocument implements IDocument { + public readonly id: string; + + private store: DocumentStore; + + constructor() { + this.id = uuid(); + } + + public get root(): INode { + const rootNode = this.getNode(FlowGramNode.Root); + if (!rootNode) { + throw new Error('Root node not found'); + } + return rootNode; + } + + public get start(): INode { + const startNode = this.nodes.find((n) => n.type === FlowGramNode.Start); + if (!startNode) { + throw new Error('Start node not found'); + } + return startNode; + } + + public get end(): INode { + const endNode = this.nodes.find((n) => n.type === FlowGramNode.End); + if (!endNode) { + throw new Error('End node not found'); + } + return endNode; + } + + public getNode(id: string): INode | null { + return this.store.nodes.get(id) ?? null; + } + + public getEdge(id: string): IEdge | null { + return this.store.edges.get(id) ?? null; + } + + public get nodes(): INode[] { + return Array.from(this.store.nodes.values()); + } + + public get edges(): IEdge[] { + return Array.from(this.store.edges.values()); + } + + public init(schema: WorkflowSchema): void { + const flattenSchema = flatSchema(schema); + this.store = createStore(flattenSchema); + } + + public dispose(): void { + this.store.edges.clear(); + this.store.nodes.clear(); + this.store.ports.clear(); + } +} diff --git a/packages/runtime/js-core/src/domain/document/entity/edge/index.test.ts b/packages/runtime/js-core/src/domain/document/entity/edge/index.test.ts new file mode 100644 index 00000000..35fe88b2 --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/entity/edge/index.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { WorkflowEdgeSchema, CreateEdgeParams, INode, IPort } from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeEdge } from '.'; + +describe('WorkflowRuntimeEdge', () => { + let edge: WorkflowRuntimeEdge; + let mockFromNode: INode; + let mockToNode: INode; + let mockParams: CreateEdgeParams; + + beforeEach(() => { + mockFromNode = { + id: 'from-node', + } as INode; + + mockToNode = { + id: 'to-node', + } as INode; + + mockParams = { + id: 'test-edge', + from: mockFromNode, + to: mockToNode, + }; + + edge = new WorkflowRuntimeEdge(mockParams); + }); + + describe('constructor', () => { + it('should initialize with provided params', () => { + expect(edge.id).toBe(mockParams.id); + expect(edge.from).toBe(mockParams.from); + expect(edge.to).toBe(mockParams.to); + }); + }); + + describe('ports', () => { + let mockFromPort: IPort; + let mockToPort: IPort; + + beforeEach(() => { + mockFromPort = { id: 'from-port' } as IPort; + mockToPort = { id: 'to-port' } as IPort; + }); + + it('should set and get fromPort correctly', () => { + edge.fromPort = mockFromPort; + expect(edge.fromPort).toBe(mockFromPort); + }); + + it('should set and get toPort correctly', () => { + edge.toPort = mockToPort; + expect(edge.toPort).toBe(mockToPort); + }); + }); + + describe('createID', () => { + it('should create ID with port IDs', () => { + const schema: WorkflowEdgeSchema = { + sourceNodeID: 'source', + sourcePortID: 'sourcePort', + targetNodeID: 'target', + targetPortID: 'targetPort', + }; + + const id = WorkflowRuntimeEdge.createID(schema); + expect(id).toBe('source:sourcePort-target:targetPort'); + }); + + it('should create ID without port IDs', () => { + const schema: WorkflowEdgeSchema = { + sourceNodeID: 'source', + targetNodeID: 'target', + }; + + const id = WorkflowRuntimeEdge.createID(schema); + expect(id).toBe('source-target'); + }); + + it('should create ID with mixed port IDs', () => { + const schemaWithSourcePort: WorkflowEdgeSchema = { + sourceNodeID: 'source', + sourcePortID: 'sourcePort', + targetNodeID: 'target', + }; + + const schemaWithTargetPort: WorkflowEdgeSchema = { + sourceNodeID: 'source', + targetNodeID: 'target', + targetPortID: 'targetPort', + }; + + expect(WorkflowRuntimeEdge.createID(schemaWithSourcePort)).toBe('source:sourcePort-target'); + expect(WorkflowRuntimeEdge.createID(schemaWithTargetPort)).toBe('source-target:targetPort'); + }); + }); +}); diff --git a/packages/runtime/js-core/src/domain/document/entity/edge/index.ts b/packages/runtime/js-core/src/domain/document/entity/edge/index.ts new file mode 100644 index 00000000..d135403e --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/entity/edge/index.ts @@ -0,0 +1,49 @@ +import { + WorkflowEdgeSchema, + CreateEdgeParams, + IEdge, + INode, + IPort, +} from '@flowgram.ai/runtime-interface'; + +export class WorkflowRuntimeEdge implements IEdge { + public readonly id: string; + + public readonly from: INode; + + public readonly to: INode; + + private _fromPort: IPort; + + private _toPort: IPort; + + constructor(params: CreateEdgeParams) { + const { id, from, to } = params; + this.id = id; + this.from = from; + this.to = to; + } + + public get fromPort() { + return this._fromPort; + } + + public set fromPort(port: IPort) { + this._fromPort = port; + } + + public get toPort() { + return this._toPort; + } + + public set toPort(port: IPort) { + this._toPort = port; + } + + public static createID(schema: WorkflowEdgeSchema): string { + const { sourceNodeID, sourcePortID, targetNodeID, targetPortID } = schema; + const sourcePart = sourcePortID ? `${sourceNodeID}:${sourcePortID}` : sourceNodeID; + const targetPart = targetPortID ? `${targetNodeID}:${targetPortID}` : targetNodeID; + return `${sourcePart}-${targetPart}`; + } +} diff --git a/packages/runtime/js-core/src/domain/document/entity/index.ts b/packages/runtime/js-core/src/domain/document/entity/index.ts new file mode 100644 index 00000000..e2556c36 --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/entity/index.ts @@ -0,0 +1,3 @@ +export { WorkflowRuntimeEdge } from './edge'; +export { WorkflowRuntimeNode } from './node'; +export { WorkflowRuntimePort } from './port'; diff --git a/packages/runtime/js-core/src/domain/document/entity/node/index.test.ts b/packages/runtime/js-core/src/domain/document/entity/node/index.test.ts new file mode 100644 index 00000000..61c98b7a --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/entity/node/index.test.ts @@ -0,0 +1,152 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { + FlowGramNode, + PositionSchema, + WorkflowPortType, + CreateNodeParams, + IEdge, + INode, + IPort, +} from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeNode } from './index'; + +describe('WorkflowRuntimeNode', () => { + let node: WorkflowRuntimeNode; + let mockParams: CreateNodeParams; + + beforeEach(() => { + mockParams = { + id: 'test-node', + type: FlowGramNode.Start, + name: 'Test Node', + position: { x: 0, y: 0 } as PositionSchema, + variable: {}, + data: { testData: 'data' }, + }; + node = new WorkflowRuntimeNode(mockParams); + }); + + describe('constructor', () => { + it('should initialize with provided params', () => { + expect(node.id).toBe(mockParams.id); + expect(node.type).toBe(mockParams.type); + expect(node.name).toBe(mockParams.name); + expect(node.position).toBe(mockParams.position); + expect(node.declare).toEqual(mockParams.variable); + expect(node.data).toEqual(mockParams.data); + }); + + it('should initialize with default values when optional params are not provided', () => { + const minimalParams = { + id: 'test-node', + type: FlowGramNode.Start, + name: 'Test Node', + position: { x: 0, y: 0 } as PositionSchema, + }; + const minimalNode = new WorkflowRuntimeNode(minimalParams); + expect(minimalNode.declare).toEqual({}); + expect(minimalNode.data).toEqual({}); + }); + }); + + describe('ports', () => { + let inputPort: IPort; + let outputPort: IPort; + + beforeEach(() => { + inputPort = { id: 'input-1', type: WorkflowPortType.Input, node, edges: [] }; + outputPort = { id: 'output-1', type: WorkflowPortType.Output, node, edges: [] }; + node.addPort(inputPort); + node.addPort(outputPort); + }); + + it('should correctly categorize input and output ports', () => { + const { inputs, outputs } = node.ports; + expect(inputs).toHaveLength(1); + expect(outputs).toHaveLength(1); + expect(inputs[0]).toBe(inputPort); + expect(outputs[0]).toBe(outputPort); + }); + }); + + describe('edges', () => { + let inputEdge: IEdge; + let outputEdge: IEdge; + let fromNode: INode; + let toNode: INode; + + beforeEach(() => { + fromNode = new WorkflowRuntimeNode({ ...mockParams, id: 'from-node' }); + toNode = new WorkflowRuntimeNode({ ...mockParams, id: 'to-node' }); + inputEdge = { + id: 'input-edge', + from: fromNode, + to: node, + fromPort: {} as IPort, + toPort: {} as IPort, + }; + outputEdge = { + id: 'output-edge', + from: node, + to: toNode, + fromPort: {} as IPort, + toPort: {} as IPort, + }; + node.addInputEdge(inputEdge); + node.addOutputEdge(outputEdge); + }); + + it('should correctly store input and output edges', () => { + const { inputs, outputs } = node.edges; + expect(inputs).toHaveLength(1); + expect(outputs).toHaveLength(1); + expect(inputs[0]).toBe(inputEdge); + expect(outputs[0]).toBe(outputEdge); + }); + + it('should update prev and next nodes when adding edges', () => { + expect(node.prev).toHaveLength(1); + expect(node.next).toHaveLength(1); + expect(node.prev[0]).toBe(fromNode); + expect(node.next[0]).toBe(toNode); + }); + }); + + describe('parent and children', () => { + let parentNode: INode; + let childNode: INode; + + beforeEach(() => { + parentNode = new WorkflowRuntimeNode({ ...mockParams, id: 'parent-node' }); + childNode = new WorkflowRuntimeNode({ ...mockParams, id: 'child-node' }); + }); + + it('should handle parent-child relationships', () => { + node.parent = parentNode; + node.addChild(childNode); + + expect(node.parent).toBe(parentNode); + expect(node.children).toHaveLength(1); + expect(node.children[0]).toBe(childNode); + }); + }); + + describe('isBranch', () => { + it('should return true when node has multiple output ports', () => { + const outputPort1 = { id: 'output-1', type: WorkflowPortType.Output, node, edges: [] }; + const outputPort2 = { id: 'output-2', type: WorkflowPortType.Output, node, edges: [] }; + node.addPort(outputPort1); + node.addPort(outputPort2); + + expect(node.isBranch).toBe(true); + }); + + it('should return false when node has one or zero output ports', () => { + const outputPort = { id: 'output-1', type: WorkflowPortType.Output, node, edges: [] }; + node.addPort(outputPort); + + expect(node.isBranch).toBe(false); + }); + }); +}); diff --git a/packages/runtime/js-core/src/domain/document/entity/node/index.ts b/packages/runtime/js-core/src/domain/document/entity/node/index.ts new file mode 100644 index 00000000..b2329c1e --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/entity/node/index.ts @@ -0,0 +1,113 @@ +import { + FlowGramNode, + PositionSchema, + CreateNodeParams, + IEdge, + INode, + IPort, + NodeVariable, + WorkflowPortType, +} from '@flowgram.ai/runtime-interface'; + +export class WorkflowRuntimeNode implements INode { + public readonly id: string; + + public readonly type: FlowGramNode; + + public readonly name: string; + + public readonly position: PositionSchema; + + public readonly declare: NodeVariable; + + public readonly data: T; + + private _parent: INode | null; + + private readonly _children: INode[]; + + private readonly _ports: IPort[]; + + private readonly _inputEdges: IEdge[]; + + private readonly _outputEdges: IEdge[]; + + private readonly _prev: INode[]; + + private readonly _next: INode[]; + + constructor(params: CreateNodeParams) { + const { id, type, name, position, variable, data } = params; + this.id = id; + this.type = type; + this.name = name; + this.position = position; + this.declare = variable ?? {}; + this.data = data ?? {}; + this._parent = null; + this._children = []; + this._ports = []; + this._inputEdges = []; + this._outputEdges = []; + this._prev = []; + this._next = []; + } + + public get ports() { + const inputs = this._ports.filter((port) => port.type === WorkflowPortType.Input); + const outputs = this._ports.filter((port) => port.type === WorkflowPortType.Output); + return { + inputs, + outputs, + }; + } + + public get edges() { + return { + inputs: this._inputEdges, + outputs: this._outputEdges, + }; + } + + public get parent() { + return this._parent; + } + + public set parent(parent: INode | null) { + this._parent = parent; + } + + public get children() { + return this._children; + } + + public addChild(child: INode) { + this._children.push(child); + } + + public addPort(port: IPort) { + this._ports.push(port); + } + + public addInputEdge(edge: IEdge) { + this._inputEdges.push(edge); + this._prev.push(edge.from); + } + + public addOutputEdge(edge: IEdge) { + this._outputEdges.push(edge); + this._next.push(edge.to); + } + + public get prev() { + return this._prev; + } + + public get next() { + return this._next; + } + + public get isBranch() { + return this.ports.outputs.length > 1; + } +} diff --git a/packages/runtime/js-core/src/domain/document/entity/port/index.test.ts b/packages/runtime/js-core/src/domain/document/entity/port/index.test.ts new file mode 100644 index 00000000..dec6b4ab --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/entity/port/index.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { WorkflowPortType, CreatePortParams, IEdge, INode } from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimePort } from '.'; + +describe('WorkflowRuntimePort', () => { + let port: WorkflowRuntimePort; + let mockNode: INode; + let mockParams: CreatePortParams; + + beforeEach(() => { + mockNode = { + id: 'test-node', + } as INode; + + mockParams = { + id: 'test-port', + node: mockNode, + type: WorkflowPortType.Input, + }; + + port = new WorkflowRuntimePort(mockParams); + }); + + describe('constructor', () => { + it('should initialize with provided params', () => { + expect(port.id).toBe(mockParams.id); + expect(port.node).toBe(mockParams.node); + expect(port.type).toBe(mockParams.type); + expect(port.edges).toEqual([]); + }); + + it('should initialize with different port types', () => { + const inputPort = new WorkflowRuntimePort({ + ...mockParams, + type: WorkflowPortType.Input, + }); + expect(inputPort.type).toBe(WorkflowPortType.Input); + + const outputPort = new WorkflowRuntimePort({ + ...mockParams, + type: WorkflowPortType.Output, + }); + expect(outputPort.type).toBe(WorkflowPortType.Output); + }); + }); + + describe('edges management', () => { + it('should add edge correctly', () => { + const mockEdge: IEdge = { + id: 'test-edge', + } as IEdge; + + port.addEdge(mockEdge); + expect(port.edges).toHaveLength(1); + expect(port.edges[0]).toBe(mockEdge); + }); + + it('should maintain edge order when adding multiple edges', () => { + const mockEdge1: IEdge = { id: 'edge-1' } as IEdge; + const mockEdge2: IEdge = { id: 'edge-2' } as IEdge; + const mockEdge3: IEdge = { id: 'edge-3' } as IEdge; + + port.addEdge(mockEdge1); + port.addEdge(mockEdge2); + port.addEdge(mockEdge3); + + expect(port.edges).toHaveLength(3); + expect(port.edges[0]).toBe(mockEdge1); + expect(port.edges[1]).toBe(mockEdge2); + expect(port.edges[2]).toBe(mockEdge3); + }); + }); +}); diff --git a/packages/runtime/js-core/src/domain/document/entity/port/index.ts b/packages/runtime/js-core/src/domain/document/entity/port/index.ts new file mode 100644 index 00000000..d3a23e97 --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/entity/port/index.ts @@ -0,0 +1,33 @@ +import { + WorkflowPortType, + CreatePortParams, + IEdge, + INode, + IPort, +} from '@flowgram.ai/runtime-interface'; + +export class WorkflowRuntimePort implements IPort { + public readonly id: string; + + public readonly node: INode; + + public readonly type: WorkflowPortType; + + private readonly _edges: IEdge[]; + + constructor(params: CreatePortParams) { + const { id, node } = params; + this.id = id; + this.node = node; + this.type = params.type; + this._edges = []; + } + + public get edges() { + return this._edges; + } + + public addEdge(edge: IEdge) { + this._edges.push(edge); + } +} diff --git a/packages/runtime/js-core/src/domain/document/index.ts b/packages/runtime/js-core/src/domain/document/index.ts new file mode 100644 index 00000000..a14cb15d --- /dev/null +++ b/packages/runtime/js-core/src/domain/document/index.ts @@ -0,0 +1 @@ +export { WorkflowRuntimeDocument } from './document'; diff --git a/packages/runtime/js-core/src/domain/engine/index.test.ts b/packages/runtime/js-core/src/domain/engine/index.test.ts new file mode 100644 index 00000000..dead56c8 --- /dev/null +++ b/packages/runtime/js-core/src/domain/engine/index.test.ts @@ -0,0 +1,53 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { TestSchemas } from '@workflow/__tests__/schemas'; +import { MockWorkflowRuntimeNodeExecutors } from '@workflow/__tests__/executor'; +import { WorkflowRuntimeExecutor } from '../executor'; +import { WorkflowRuntimeEngine } from './index'; + +let engine: WorkflowRuntimeEngine; + +beforeEach(() => { + const Executor = new WorkflowRuntimeExecutor(MockWorkflowRuntimeNodeExecutors); + engine = new WorkflowRuntimeEngine({ + Executor, + }); +}); + +describe('WorkflowRuntimeEngine', () => { + it('should create a WorkflowRuntimeEngine', () => { + expect(engine).toBeDefined(); + }); + + it('should execute a workflow with input', async () => { + const { processing } = engine.invoke({ + schema: TestSchemas.basicSchema, + inputs: { + model_name: 'ai-model', + llm_settings: { + temperature: 0.5, + }, + prompt: 'How are you?', + }, + }); + const result = await processing; + expect(result).toStrictEqual({ + llm_res: `Hi, I'm an AI assistant, my name is ai-model, temperature is 0.5, system prompt is "You are a helpful AI assistant.", prompt is "How are you?"`, + llm_prompt: 'How are you?', + }); + }); + + it('should execute a workflow with branch', async () => { + const { processing } = engine.invoke({ + schema: TestSchemas.branchSchema, + inputs: { + model_id: 1, + prompt: 'Tell me a joke', + }, + }); + const result = await processing; + expect(result).toStrictEqual({ + m1_res: `Hi, I'm an AI assistant, my name is AI_MODEL_1, temperature is 0.5, system prompt is "I'm Model 1.", prompt is "Tell me a joke"`, + }); + }); +}); diff --git a/packages/runtime/js-core/src/domain/engine/index.ts b/packages/runtime/js-core/src/domain/engine/index.ts new file mode 100644 index 00000000..0fce5c33 --- /dev/null +++ b/packages/runtime/js-core/src/domain/engine/index.ts @@ -0,0 +1,133 @@ +import { + EngineServices, + IEngine, + IExecutor, + INode, + WorkflowOutputs, + IContext, + InvokeParams, + ITask, + FlowGramNode, +} from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeTask } from '../task'; +import { WorkflowRuntimeContext } from '../context'; +import { WorkflowRuntimeContainer } from '../container'; + +export class WorkflowRuntimeEngine implements IEngine { + private readonly executor: IExecutor; + + constructor(service: EngineServices) { + this.executor = service.Executor; + } + + public invoke(params: InvokeParams): ITask { + const context = WorkflowRuntimeContext.create(); + context.init(params); + const processing = this.process(context); + processing.then(() => { + context.dispose(); + }); + return WorkflowRuntimeTask.create({ + processing, + context, + }); + } + + public async executeNode(params: { context: IContext; node: INode }) { + const { node, context } = params; + if (!this.canExecuteNode({ node, context })) { + return; + } + context.statusCenter.nodeStatus(node.id).process(); + try { + const inputs = context.state.getNodeInputs(node); + const snapshot = context.snapshotCenter.create({ + nodeID: node.id, + data: node.data, + inputs, + }); + const result = await this.executor.execute({ + node, + inputs, + runtime: context, + container: WorkflowRuntimeContainer.instance, + }); + if (context.statusCenter.workflow.terminated) { + return; + } + const { outputs, branch } = result; + snapshot.addData({ outputs, branch }); + context.state.setNodeOutputs({ node, outputs }); + context.state.addExecutedNode(node); + context.statusCenter.nodeStatus(node.id).success(); + const nextNodes = this.getNextNodes({ node, branch, context }); + await this.executeNext({ node, nextNodes, context }); + } catch (e) { + context.statusCenter.nodeStatus(node.id).fail(); + console.error(e); + return; + } + } + + private async process(context: IContext): Promise { + const startNode = context.document.start; + context.statusCenter.workflow.process(); + try { + await this.executeNode({ node: startNode, context }); + const outputs = context.ioCenter.outputs; + context.statusCenter.workflow.success(); + return outputs; + } catch (e) { + context.statusCenter.workflow.fail(); + throw e; + } + } + + private canExecuteNode(params: { context: IContext; node: INode }) { + const { node, context } = params; + const prevNodes = node.prev; + if (prevNodes.length === 0) { + return true; + } + return prevNodes.every((prevNode) => context.state.isExecutedNode(prevNode)); + } + + private getNextNodes(params: { context: IContext; node: INode; branch?: string }) { + const { node, branch, context } = params; + const allNextNodes = node.next; + if (!branch) { + return allNextNodes; + } + const targetPort = node.ports.outputs.find((port) => port.id === branch); + if (!targetPort) { + throw new Error(`branch ${branch} not found`); + } + const nextNodeIDs: Set = new Set(targetPort.edges.map((edge) => edge.to.id)); + const nextNodes = allNextNodes.filter((nextNode) => nextNodeIDs.has(nextNode.id)); + const skipNodes = allNextNodes.filter((nextNode) => !nextNodeIDs.has(nextNode.id)); + skipNodes.forEach((skipNode) => { + context.state.addExecutedNode(skipNode); + }); + return nextNodes; + } + + private async executeNext(params: { context: IContext; node: INode; nextNodes: INode[] }) { + const { context, node, nextNodes } = params; + if (node.type === FlowGramNode.End) { + return; + } + if (nextNodes.length === 0) { + // throw new Error(`node ${node.id} has no next nodes`); // inside loop node may have no next nodes + return; + } + await Promise.all( + nextNodes.map((nextNode) => + this.executeNode({ + node: nextNode, + context, + }) + ) + ); + } +} diff --git a/packages/runtime/js-core/src/domain/executor/index.ts b/packages/runtime/js-core/src/domain/executor/index.ts new file mode 100644 index 00000000..e5c46518 --- /dev/null +++ b/packages/runtime/js-core/src/domain/executor/index.ts @@ -0,0 +1,33 @@ +import { FlowGramNode } from '@flowgram.ai/runtime-interface'; +import { + ExecutionContext, + ExecutionResult, + IExecutor, + INodeExecutor, + INodeExecutorFactory, +} from '@flowgram.ai/runtime-interface'; + +export class WorkflowRuntimeExecutor implements IExecutor { + private nodeExecutors: Map = new Map(); + + constructor(nodeExecutors: INodeExecutorFactory[]) { + // register node executors + nodeExecutors.forEach((executor) => { + this.register(new executor()); + }); + } + + public register(executor: INodeExecutor): void { + this.nodeExecutors.set(executor.type, executor); + } + + public async execute(context: ExecutionContext): Promise { + const nodeType = context.node.type; + const nodeExecutor = this.nodeExecutors.get(nodeType); + if (!nodeExecutor) { + throw new Error(`no executor found for node type ${nodeType}`); + } + const output = await nodeExecutor.execute(context); + return output; + } +} diff --git a/packages/runtime/js-core/src/domain/io-center/index.ts b/packages/runtime/js-core/src/domain/io-center/index.ts new file mode 100644 index 00000000..316f99d0 --- /dev/null +++ b/packages/runtime/js-core/src/domain/io-center/index.ts @@ -0,0 +1,36 @@ +import { IIOCenter, IOData, WorkflowInputs, WorkflowOutputs } from '@flowgram.ai/runtime-interface'; + +export class WorkflowRuntimeIOCenter implements IIOCenter { + private _inputs: WorkflowInputs; + + private _outputs: WorkflowOutputs; + + public init(inputs: WorkflowInputs): void { + this.setInputs(inputs); + } + + public dispose(): void {} + + public get inputs(): WorkflowInputs { + return this._inputs ?? {}; + } + + public get outputs(): WorkflowOutputs { + return this._outputs ?? {}; + } + + public setInputs(inputs: WorkflowInputs): void { + this._inputs = inputs; + } + + public setOutputs(outputs: WorkflowOutputs): void { + this._outputs = outputs; + } + + public export(): IOData { + return { + inputs: this._inputs, + outputs: this._outputs, + }; + } +} diff --git a/packages/runtime/js-core/src/domain/report/index.ts b/packages/runtime/js-core/src/domain/report/index.ts new file mode 100644 index 00000000..de1b3c42 --- /dev/null +++ b/packages/runtime/js-core/src/domain/report/index.ts @@ -0,0 +1 @@ +export { WorkflowRuntimeReporter } from './reporter'; diff --git a/packages/runtime/js-core/src/domain/report/report-value-object/index.ts b/packages/runtime/js-core/src/domain/report/report-value-object/index.ts new file mode 100644 index 00000000..879faa52 --- /dev/null +++ b/packages/runtime/js-core/src/domain/report/report-value-object/index.ts @@ -0,0 +1,10 @@ +import { IReport, VOData } from '@flowgram.ai/runtime-interface'; + +import { uuid } from '@infra/utils'; + +export namespace WorkflowRuntimeReport { + export const create = (params: VOData): IReport => ({ + id: uuid(), + ...params, + }); +} diff --git a/packages/runtime/js-core/src/domain/report/reporter/index.ts b/packages/runtime/js-core/src/domain/report/reporter/index.ts new file mode 100644 index 00000000..5d901f42 --- /dev/null +++ b/packages/runtime/js-core/src/domain/report/reporter/index.ts @@ -0,0 +1,49 @@ +import { + ISnapshotCenter, + IReporter, + IStatusCenter, + IIOCenter, + IReport, + NodeReport, +} from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeReport } from '../report-value-object'; + +export class WorkflowRuntimeReporter implements IReporter { + constructor( + public readonly ioCenter: IIOCenter, + public readonly snapshotCenter: ISnapshotCenter, + public readonly statusCenter: IStatusCenter + ) {} + + public init(): void {} + + public dispose(): void {} + + public export(): IReport { + const report = WorkflowRuntimeReport.create({ + inputs: this.ioCenter.inputs, + outputs: this.ioCenter.outputs, + workflowStatus: this.statusCenter.workflow.export(), + reports: this.nodeReports(), + }); + return report; + } + + private nodeReports(): Record { + const reports: Record = {}; + const statuses = this.statusCenter.exportNodeStatus(); + const snapshots = this.snapshotCenter.export(); + Object.keys(statuses).forEach((nodeID) => { + const status = statuses[nodeID]; + const nodeSnapshots = snapshots[nodeID] || []; + const nodeReport: NodeReport = { + id: nodeID, + ...status, + snapshots: nodeSnapshots, + }; + reports[nodeID] = nodeReport; + }); + return reports; + } +} diff --git a/packages/runtime/js-core/src/domain/snapshot/index.ts b/packages/runtime/js-core/src/domain/snapshot/index.ts new file mode 100644 index 00000000..2b8c57af --- /dev/null +++ b/packages/runtime/js-core/src/domain/snapshot/index.ts @@ -0,0 +1 @@ +export { WorkflowRuntimeSnapshotCenter } from './snapshot-center'; diff --git a/packages/runtime/js-core/src/domain/snapshot/snapshot-center/index.ts b/packages/runtime/js-core/src/domain/snapshot/snapshot-center/index.ts new file mode 100644 index 00000000..43480bee --- /dev/null +++ b/packages/runtime/js-core/src/domain/snapshot/snapshot-center/index.ts @@ -0,0 +1,44 @@ +import { Snapshot, ISnapshotCenter, SnapshotData, ISnapshot } from '@flowgram.ai/runtime-interface'; + +import { uuid } from '@infra/utils'; +import { WorkflowRuntimeSnapshot } from '../snapshot-entity'; + +export class WorkflowRuntimeSnapshotCenter implements ISnapshotCenter { + public readonly id: string; + + private snapshots: ISnapshot[]; + + constructor() { + this.id = uuid(); + } + + public create(snapshotData: Partial): ISnapshot { + const snapshot = WorkflowRuntimeSnapshot.create(snapshotData); + this.snapshots.push(snapshot); + return snapshot; + } + + public init(): void { + this.snapshots = []; + } + + public dispose(): void { + // because the data is not persisted, do not clear the execution result + } + + public exportAll(): Snapshot[] { + return this.snapshots.slice().map((snapshot) => snapshot.export()); + } + + public export(): Record { + const result: Record = {}; + this.exportAll().forEach((snapshot) => { + if (result[snapshot.nodeID]) { + result[snapshot.nodeID].push(snapshot); + } else { + result[snapshot.nodeID] = [snapshot]; + } + }); + return result; + } +} diff --git a/packages/runtime/js-core/src/domain/snapshot/snapshot-entity/index.ts b/packages/runtime/js-core/src/domain/snapshot/snapshot-entity/index.ts new file mode 100644 index 00000000..f6d403c9 --- /dev/null +++ b/packages/runtime/js-core/src/domain/snapshot/snapshot-entity/index.ts @@ -0,0 +1,35 @@ +import { ISnapshot, Snapshot, SnapshotData } from '@flowgram.ai/runtime-interface'; + +import { uuid } from '@infra/utils'; + +export class WorkflowRuntimeSnapshot implements ISnapshot { + public readonly id: string; + + public readonly data: Partial; + + public constructor(data: Partial) { + this.id = uuid(); + this.data = data; + } + + public addData(data: Partial): void { + Object.assign(this.data, data); + } + + public validate(): boolean { + const required = ['nodeID', 'inputs', 'outputs', 'data'] as (keyof SnapshotData)[]; + return required.every((key) => this.data[key] !== undefined); + } + + public export(): Snapshot { + const snapshot: Snapshot = { + id: this.id, + ...this.data, + } as Snapshot; + return snapshot; + } + + public static create(params: Partial): ISnapshot { + return new WorkflowRuntimeSnapshot(params); + } +} diff --git a/packages/runtime/js-core/src/domain/state/index.ts b/packages/runtime/js-core/src/domain/state/index.ts new file mode 100644 index 00000000..0b1af1dd --- /dev/null +++ b/packages/runtime/js-core/src/domain/state/index.ts @@ -0,0 +1,134 @@ +import { isNil } from 'lodash-es'; +import { + IState, + IFlowConstantRefValue, + IFlowRefValue, + IVariableParseResult, + INode, + WorkflowInputs, + WorkflowOutputs, + IVariableStore, + WorkflowVariableType, +} from '@flowgram.ai/runtime-interface'; + +import { uuid, WorkflowRuntimeType } from '@infra/utils'; + +export class WorkflowRuntimeState implements IState { + public readonly id: string; + + private executedNodes: Set; + + constructor(public readonly variableStore: IVariableStore) { + this.id = uuid(); + } + + public init(): void { + this.executedNodes = new Set(); + } + + public dispose(): void { + this.executedNodes.clear(); + } + + public getNodeInputs(node: INode): WorkflowInputs { + const inputsDeclare = node.declare.inputs; + const inputsValues = node.declare.inputsValues; + if (!inputsDeclare || !inputsValues) { + return {}; + } + return Object.entries(inputsValues).reduce((prev, [key, inputValue]) => { + const typeInfo = inputsDeclare.properties?.[key]; + if (!typeInfo) { + return prev; + } + const expectType = typeInfo.type as WorkflowVariableType; + // get value + const result = this.parseValue(inputValue); + if (!result) { + return prev; + } + const { value, type } = result; + if (!WorkflowRuntimeType.isTypeEqual(type, expectType)) { + return prev; + } + prev[key] = value; + return prev; + }, {} as WorkflowInputs); + } + + public setNodeOutputs(params: { node: INode; outputs: WorkflowOutputs }): void { + const { node, outputs } = params; + const outputsDeclare = node.declare.outputs; + // TODO validation service type check, deeply compare input & schema + if (!outputsDeclare) { + return; + } + Object.entries(outputs).forEach(([key, value]) => { + const typeInfo = outputsDeclare.properties?.[key]; + if (!typeInfo) { + return; + } + const type = typeInfo.type as WorkflowVariableType; + const itemsType = typeInfo.items?.type as WorkflowVariableType; + // create variable + this.variableStore.setVariable({ + nodeID: node.id, + key, + value, + type, + itemsType, + }); + }); + } + + public parseRef(ref: IFlowRefValue): IVariableParseResult | null { + if (ref?.type !== 'ref') { + throw new Error(`invalid ref value: ${ref}`); + } + if (!ref.content || ref.content.length < 2) { + return null; + } + const [nodeID, variableKey, ...variablePath] = ref.content; + const result = this.variableStore.getValue({ + nodeID, + variableKey, + variablePath, + }); + if (!result) { + return null; + } + return result; + } + + public parseValue(flowValue: IFlowConstantRefValue): IVariableParseResult | null { + if (!flowValue?.type) { + throw new Error(`invalid flow value type: ${(flowValue as any).type}`); + } + // constant + if (flowValue.type === 'constant') { + const value = flowValue.content as T; + const type = WorkflowRuntimeType.getWorkflowType(value); + if (isNil(value) || !type) { + return null; + } + return { + value, + type, + }; + } + // ref + if (flowValue.type === 'ref') { + return this.parseRef(flowValue); + } + // unknown type + throw new Error(`unknown flow value type: ${(flowValue as any).type}`); + } + + public isExecutedNode(node: INode): boolean { + return this.executedNodes.has(node.id); + } + + public addExecutedNode(node: INode): void { + this.executedNodes.add(node.id); + } +} diff --git a/packages/runtime/js-core/src/domain/status/index.ts b/packages/runtime/js-core/src/domain/status/index.ts new file mode 100644 index 00000000..c0db0c02 --- /dev/null +++ b/packages/runtime/js-core/src/domain/status/index.ts @@ -0,0 +1 @@ +export { WorkflowRuntimeStatusCenter } from './status-center'; diff --git a/packages/runtime/js-core/src/domain/status/status-center/index.ts b/packages/runtime/js-core/src/domain/status/status-center/index.ts new file mode 100644 index 00000000..3c9a693b --- /dev/null +++ b/packages/runtime/js-core/src/domain/status/status-center/index.ts @@ -0,0 +1,50 @@ +import { IStatus, IStatusCenter, StatusData, WorkflowStatus } from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeStatus } from '../status-entity'; + +export class WorkflowRuntimeStatusCenter implements IStatusCenter { + private _workflowStatus: IStatus; + + private _nodeStatus: Map; + + public startTime: number; + + public endTime?: number; + + public init(): void { + this._workflowStatus = WorkflowRuntimeStatus.create(); + this._nodeStatus = new Map(); + } + + public dispose(): void { + // because the data is not persisted, do not clear the execution result + } + + public get workflow(): IStatus { + return this._workflowStatus; + } + + public get workflowStatus(): IStatus { + return this._workflowStatus; + } + + public nodeStatus(nodeID: string): IStatus { + if (!this._nodeStatus.has(nodeID)) { + this._nodeStatus.set(nodeID, WorkflowRuntimeStatus.create()); + } + const status = this._nodeStatus.get(nodeID)!; + return status; + } + + public getStatusNodeIDs(status: WorkflowStatus): string[] { + return Array.from(this._nodeStatus.entries()) + .filter(([, nodeStatus]) => nodeStatus.status === status) + .map(([nodeID]) => nodeID); + } + + public exportNodeStatus(): Record { + return Object.fromEntries( + Array.from(this._nodeStatus.entries()).map(([nodeID, status]) => [nodeID, status.export()]) + ); + } +} diff --git a/packages/runtime/js-core/src/domain/status/status-entity/index.ts b/packages/runtime/js-core/src/domain/status/status-entity/index.ts new file mode 100644 index 00000000..27edc535 --- /dev/null +++ b/packages/runtime/js-core/src/domain/status/status-entity/index.ts @@ -0,0 +1,91 @@ +import { IStatus, StatusData, WorkflowStatus } from '@flowgram.ai/runtime-interface'; + +import { uuid } from '@infra/utils'; + +export class WorkflowRuntimeStatus implements IStatus { + public readonly id: string; + + private _status: WorkflowStatus; + + private _startTime: number; + + private _endTime?: number; + + constructor() { + this.id = uuid(); + this._status = WorkflowStatus.Pending; + } + + public get status(): WorkflowStatus { + return this._status; + } + + public get terminated(): boolean { + return [WorkflowStatus.Succeeded, WorkflowStatus.Failed, WorkflowStatus.Canceled].includes( + this.status + ); + } + + public get startTime(): number { + return this._startTime; + } + + public get endTime(): number | undefined { + return this._endTime; + } + + public get timeCost(): number { + if (!this.startTime) { + return 0; + } + if (this.endTime) { + return this.endTime - this.startTime; + } + return Date.now() - this.startTime; + } + + public process(): void { + this._status = WorkflowStatus.Processing; + this._startTime = Date.now(); + this._endTime = undefined; + } + + public success(): void { + if (this.terminated) { + return; + } + this._status = WorkflowStatus.Succeeded; + this._endTime = Date.now(); + } + + public fail(): void { + if (this.terminated) { + return; + } + this._status = WorkflowStatus.Failed; + this._endTime = Date.now(); + } + + public cancel(): void { + if (this.terminated) { + return; + } + this._status = WorkflowStatus.Canceled; + this._endTime = Date.now(); + } + + public export(): StatusData { + return { + status: this.status, + terminated: this.terminated, + startTime: this.startTime, + endTime: this.endTime, + timeCost: this.timeCost, + }; + } + + public static create(): WorkflowRuntimeStatus { + const status = new WorkflowRuntimeStatus(); + return status; + } +} diff --git a/packages/runtime/js-core/src/domain/task/index.ts b/packages/runtime/js-core/src/domain/task/index.ts new file mode 100644 index 00000000..de07a7f6 --- /dev/null +++ b/packages/runtime/js-core/src/domain/task/index.ts @@ -0,0 +1,35 @@ +import { + IContext, + ITask, + TaskParams, + WorkflowOutputs, + WorkflowStatus, +} from '@flowgram.ai/runtime-interface'; + +import { uuid } from '@infra/utils'; + +export class WorkflowRuntimeTask implements ITask { + public readonly id: string; + + public readonly processing: Promise; + + public readonly context: IContext; + + constructor(params: TaskParams) { + this.id = uuid(); + this.context = params.context; + this.processing = params.processing; + } + + public cancel(): void { + this.context.statusCenter.workflow.cancel(); + const cancelNodeIDs = this.context.statusCenter.getStatusNodeIDs(WorkflowStatus.Processing); + cancelNodeIDs.forEach((nodeID) => { + this.context.statusCenter.nodeStatus(nodeID).cancel(); + }); + } + + public static create(params: TaskParams): WorkflowRuntimeTask { + return new WorkflowRuntimeTask(params); + } +} diff --git a/packages/runtime/js-core/src/domain/validation/index.ts b/packages/runtime/js-core/src/domain/validation/index.ts new file mode 100644 index 00000000..c3c64a79 --- /dev/null +++ b/packages/runtime/js-core/src/domain/validation/index.ts @@ -0,0 +1,18 @@ +import { WorkflowSchema, IValidation, ValidationResult } from '@flowgram.ai/runtime-interface'; + +export class WorkflowRuntimeValidation implements IValidation { + validate(schema: WorkflowSchema): ValidationResult { + // TODO + // 检查成环 + // 检查边的节点是否存在 + // 检查跨层级连线 + // 检查是否只有一个开始节点和一个结束节点 + // 检查开始节点是否在根节点 + // 检查结束节点是否在根节点 + + // 注册节点检查器 + return { + valid: true, + }; + } +} diff --git a/packages/runtime/js-core/src/domain/variable/index.ts b/packages/runtime/js-core/src/domain/variable/index.ts new file mode 100644 index 00000000..da775ecb --- /dev/null +++ b/packages/runtime/js-core/src/domain/variable/index.ts @@ -0,0 +1 @@ +export { WorkflowRuntimeVariableStore } from './variable-store'; diff --git a/packages/runtime/js-core/src/domain/variable/variable-store/index.test.ts b/packages/runtime/js-core/src/domain/variable/variable-store/index.test.ts new file mode 100644 index 00000000..61b43405 --- /dev/null +++ b/packages/runtime/js-core/src/domain/variable/variable-store/index.test.ts @@ -0,0 +1,201 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { IVariableStore, WorkflowVariableType } from '@flowgram.ai/runtime-interface'; + +import { WorkflowRuntimeVariableStore } from './index'; + +describe('WorkflowRuntimeVariableStore', () => { + let variableStore: IVariableStore; + + beforeEach(() => { + variableStore = new WorkflowRuntimeVariableStore(); + variableStore.init(); + }); + + it('should create a store with unique id', () => { + const store1 = new WorkflowRuntimeVariableStore(); + const store2 = new WorkflowRuntimeVariableStore(); + expect(store1.id).toBeTruthy(); + expect(store2.id).toBeTruthy(); + expect(store1.id).not.toBe(store2.id); + }); + + describe('set', () => { + it('should set variable', () => { + const value = { foo: 'bar' }; + variableStore.setVariable({ + nodeID: 'node1', + key: 'var1', + value, + type: WorkflowVariableType.Object, + }); + + expect(variableStore.store.get('node1')?.get('var1')?.value).toEqual(value); + }); + + it('should update existing variable', () => { + variableStore.setVariable({ + nodeID: 'node1', + key: 'var1', + value: { foo: 'bar' }, + type: WorkflowVariableType.Object, + }); + + variableStore.setVariable({ + nodeID: 'node1', + key: 'var1', + value: { baz: 'qux' }, + type: WorkflowVariableType.Object, + }); + + expect(variableStore.store.get('node1')?.get('var1')?.value).toEqual({ baz: 'qux' }); + }); + }); + + describe('setValue', () => { + it('should set value without path', () => { + const value = { foo: 'bar' }; + variableStore.setValue({ + nodeID: 'node1', + variableKey: 'var1', + value, + }); + + expect(variableStore.store.get('node1')?.get('var1')?.value).toEqual(value); + }); + + it('should set value with path', () => { + variableStore.setValue({ + nodeID: 'node1', + variableKey: 'var1', + variablePath: ['foo', 'bar'], + value: 'baz', + }); + + expect(variableStore.store.get('node1')?.get('var1')?.value).toEqual({ + foo: { bar: 'baz' }, + }); + }); + + it('should update existing value', () => { + variableStore.setValue({ + nodeID: 'node1', + variableKey: 'var1', + value: { foo: 'bar' }, + }); + + variableStore.setValue({ + nodeID: 'node1', + variableKey: 'var1', + value: { baz: 'qux' }, + }); + + expect(variableStore.store.get('node1')?.get('var1')?.value).toEqual({ baz: 'qux' }); + }); + }); + + describe('get', () => { + beforeEach(() => { + variableStore.setValue({ + nodeID: 'node1', + variableKey: 'var1', + value: { foo: { bar: 'baz' } }, + }); + }); + + it('should get value without path', () => { + const result = variableStore.getValue({ + nodeID: 'node1', + variableKey: 'var1', + }); + + expect(result?.value).toEqual({ foo: { bar: 'baz' } }); + }); + + it('should get value with path', () => { + const result = variableStore.getValue({ + nodeID: 'node1', + variableKey: 'var1', + variablePath: ['foo', 'bar'], + }); + + expect(result?.value).toBe('baz'); + }); + + it('should get value with empty path', () => { + const result = variableStore.getValue({ + nodeID: 'node1', + variableKey: 'var1', + variablePath: [], + }); + + expect(result?.value).toStrictEqual({ foo: { bar: 'baz' } }); + }); + + it('should get value with undefined path', () => { + const result = variableStore.getValue({ + nodeID: 'node1', + variableKey: 'var1', + }); + + expect(result?.value).toStrictEqual({ foo: { bar: 'baz' } }); + }); + + it('should return undefined for non-existent node', () => { + const result = variableStore.getValue({ + nodeID: 'non-existent', + variableKey: 'var1', + }); + + expect(result?.value).toBeUndefined(); + }); + + it('should return undefined for non-existent variable', () => { + const result = variableStore.getValue({ + nodeID: 'node1', + variableKey: 'non-existent', + }); + + expect(result?.value).toBeUndefined(); + }); + + it('should return undefined for non-existent path', () => { + const result = variableStore.getValue({ + nodeID: 'node1', + variableKey: 'var1', + variablePath: ['non', 'existent'], + }); + + expect(result?.value).toBeUndefined(); + }); + + it('should get number value', () => { + variableStore.setVariable({ + nodeID: 'start_0', + key: 'llm_settings', + value: { temperature: 0.5 }, + type: WorkflowVariableType.Object, + }); + + const result = variableStore.getValue({ + nodeID: 'start_0', + variableKey: 'llm_settings', + variablePath: ['temperature'], + }); + + expect(result?.value).toBe(0.5); + }); + + it('should return 0', () => { + variableStore.setValue({ + nodeID: 'node1', + variableKey: 'var1', + value: 0, + }); + const result = variableStore.getValue({ + nodeID: 'node1', + variableKey: 'var1', + }); + expect(result?.value).toBe(0); + }); + }); +}); diff --git a/packages/runtime/js-core/src/domain/variable/variable-store/index.ts b/packages/runtime/js-core/src/domain/variable/variable-store/index.ts new file mode 100644 index 00000000..fa5064c9 --- /dev/null +++ b/packages/runtime/js-core/src/domain/variable/variable-store/index.ts @@ -0,0 +1,135 @@ +import { get, set } from 'lodash-es'; +import { + WorkflowVariableType, + IVariableStore, + IVariable, + IVariableParseResult, +} from '@flowgram.ai/runtime-interface'; + +import { uuid, WorkflowRuntimeType } from '@infra/utils'; +import { WorkflowRuntimeVariable } from '../variable-value-object'; + +export class WorkflowRuntimeVariableStore implements IVariableStore { + public readonly id: string; + + private parent?: WorkflowRuntimeVariableStore; + + constructor() { + this.id = uuid(); + } + + public store: Map>; + + public init(): void { + this.store = new Map(); + } + + public dispose(): void { + this.store.clear(); + } + + public setParent(parent: IVariableStore): void { + this.parent = parent as WorkflowRuntimeVariableStore; + } + + public globalGet(nodeID: string): Map | undefined { + const store = this.store.get(nodeID); + if (!store && this.parent) { + return this.parent.globalGet(nodeID); + } + return store; + } + + public setVariable(params: { + nodeID: string; + key: string; + value: Object; + type: WorkflowVariableType; + itemsType?: WorkflowVariableType; + }): void { + const { nodeID, key, value, type, itemsType } = params; + if (!this.store.has(nodeID)) { + // create node store + this.store.set(nodeID, new Map()); + } + const nodeStore = this.store.get(nodeID)!; + // create variable store + const variable = WorkflowRuntimeVariable.create({ + nodeID, + key, + value, + type, // TODO check type + itemsType, // TODO check is array + }); + nodeStore.set(key, variable); + } + + public setValue(params: { + nodeID: string; + variableKey: string; + variablePath?: string[]; + value: Object; + }): void { + const { nodeID, variableKey, variablePath, value } = params; + if (!this.store.has(nodeID)) { + // create node store + this.store.set(nodeID, new Map()); + } + const nodeStore = this.store.get(nodeID)!; + if (!nodeStore.has(variableKey)) { + // create variable store + const variable = WorkflowRuntimeVariable.create({ + nodeID, + key: variableKey, + value: {}, + type: WorkflowVariableType.Object, + }); + nodeStore.set(variableKey, variable); + } + const variable = nodeStore.get(variableKey)!; + if (!variablePath) { + variable.value = value; + return; + } + set(variable.value, variablePath, value); + } + + public getValue(params: { + nodeID: string; + variableKey: string; + variablePath?: string[]; + }): IVariableParseResult | null { + const { nodeID, variableKey, variablePath } = params; + const variable = this.globalGet(nodeID)?.get(variableKey); + if (!variable) { + return null; + } + if (!variablePath || variablePath.length === 0) { + return { + value: variable.value as T, + type: variable.type, + itemsType: variable.itemsType, + }; + } + const value = get(variable.value, variablePath) as T; + const type = WorkflowRuntimeType.getWorkflowType(value); + if (!type) { + return null; + } + if (type === WorkflowVariableType.Array && Array.isArray(value)) { + const itemsType = WorkflowRuntimeType.getWorkflowType(value[0]); + if (!itemsType) { + return null; + } + return { + value, + type, + itemsType, + }; + } + return { + value, + type, + }; + } +} diff --git a/packages/runtime/js-core/src/domain/variable/variable-value-object/index.ts b/packages/runtime/js-core/src/domain/variable/variable-value-object/index.ts new file mode 100644 index 00000000..33acd7e2 --- /dev/null +++ b/packages/runtime/js-core/src/domain/variable/variable-value-object/index.ts @@ -0,0 +1,10 @@ +import { IVariable, VOData } from '@flowgram.ai/runtime-interface'; + +import { uuid } from '@infra/utils'; + +export namespace WorkflowRuntimeVariable { + export const create = (params: VOData): IVariable => ({ + id: uuid(), + ...params, + }); +} diff --git a/packages/runtime/js-core/src/index.ts b/packages/runtime/js-core/src/index.ts new file mode 100644 index 00000000..b1c13e73 --- /dev/null +++ b/packages/runtime/js-core/src/index.ts @@ -0,0 +1 @@ +export * from './api'; diff --git a/packages/runtime/js-core/src/infrastructure/index.ts b/packages/runtime/js-core/src/infrastructure/index.ts new file mode 100644 index 00000000..04bca77e --- /dev/null +++ b/packages/runtime/js-core/src/infrastructure/index.ts @@ -0,0 +1 @@ +export * from './utils'; diff --git a/packages/runtime/js-core/src/infrastructure/utils/delay.ts b/packages/runtime/js-core/src/infrastructure/utils/delay.ts new file mode 100644 index 00000000..4557adc6 --- /dev/null +++ b/packages/runtime/js-core/src/infrastructure/utils/delay.ts @@ -0,0 +1 @@ +export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/runtime/js-core/src/infrastructure/utils/index.ts b/packages/runtime/js-core/src/infrastructure/utils/index.ts new file mode 100644 index 00000000..1f176ec7 --- /dev/null +++ b/packages/runtime/js-core/src/infrastructure/utils/index.ts @@ -0,0 +1,3 @@ +export { delay } from './delay'; +export { uuid } from './uuid'; +export { WorkflowRuntimeType } from './runtime-type'; diff --git a/packages/runtime/js-core/src/infrastructure/utils/runtime-type.ts b/packages/runtime/js-core/src/infrastructure/utils/runtime-type.ts new file mode 100644 index 00000000..96e09668 --- /dev/null +++ b/packages/runtime/js-core/src/infrastructure/utils/runtime-type.ts @@ -0,0 +1,60 @@ +import { WorkflowVariableType } from '@flowgram.ai/runtime-interface'; + +export namespace WorkflowRuntimeType { + export const getWorkflowType = (value?: unknown): WorkflowVariableType | null => { + // 处理 null 和 undefined 的情况 + if (value === null || value === undefined) { + return WorkflowVariableType.Null; + } + + // 处理基本类型 + if (typeof value === 'string') { + return WorkflowVariableType.String; + } + + if (typeof value === 'boolean') { + return WorkflowVariableType.Boolean; + } + + if (typeof value === 'number') { + if (Number.isInteger(value)) { + return WorkflowVariableType.Integer; + } + return WorkflowVariableType.Number; + } + + // 处理数组 + if (Array.isArray(value)) { + return WorkflowVariableType.Array; + } + + // 处理普通对象 + if (typeof value === 'object') { + return WorkflowVariableType.Object; + } + + return null; + }; + + export const isMatchWorkflowType = (value: unknown, type: WorkflowVariableType): boolean => { + const workflowType = getWorkflowType(value); + if (!workflowType) { + return false; + } + return workflowType === type; + }; + + export const isTypeEqual = ( + leftType: WorkflowVariableType, + rightType: WorkflowVariableType + ): boolean => { + // 处理 Number 和 Integer 等价的情况 + if ( + (leftType === WorkflowVariableType.Number && rightType === WorkflowVariableType.Integer) || + (leftType === WorkflowVariableType.Integer && rightType === WorkflowVariableType.Number) + ) { + return true; + } + return leftType === rightType; + }; +} diff --git a/packages/runtime/js-core/src/infrastructure/utils/uuid.ts b/packages/runtime/js-core/src/infrastructure/utils/uuid.ts new file mode 100644 index 00000000..8eb7c015 --- /dev/null +++ b/packages/runtime/js-core/src/infrastructure/utils/uuid.ts @@ -0,0 +1,3 @@ +import { v4 } from 'uuid'; + +export const uuid = v4; diff --git a/packages/runtime/js-core/src/nodes/condition/handlers/array.ts b/packages/runtime/js-core/src/nodes/condition/handlers/array.ts new file mode 100644 index 00000000..bb09b26a --- /dev/null +++ b/packages/runtime/js-core/src/nodes/condition/handlers/array.ts @@ -0,0 +1,16 @@ +import { isNil } from 'lodash-es'; + +import { ConditionHandler, ConditionOperation } from '../type'; + +export const conditionArrayHandler: ConditionHandler = (condition) => { + const { operator } = condition; + const leftValue = condition.leftValue as object; + // Switch case share scope, so we need to use if else here + if (operator === ConditionOperation.IS_EMPTY) { + return isNil(leftValue); + } + if (operator === ConditionOperation.IS_NOT_EMPTY) { + return !isNil(leftValue); + } + return false; +}; diff --git a/packages/runtime/js-core/src/nodes/condition/handlers/boolean.ts b/packages/runtime/js-core/src/nodes/condition/handlers/boolean.ts new file mode 100644 index 00000000..01ddd1ee --- /dev/null +++ b/packages/runtime/js-core/src/nodes/condition/handlers/boolean.ts @@ -0,0 +1,38 @@ +import { isNil } from 'lodash-es'; + +import { ConditionHandler, ConditionOperation } from '../type'; + +export const conditionBooleanHandler: ConditionHandler = (condition) => { + const { operator } = condition; + const leftValue = condition.leftValue as boolean; + // Switch case share scope, so we need to use if else here + if (operator === ConditionOperation.EQ) { + const rightValue = condition.rightValue as boolean; + return leftValue === rightValue; + } + if (operator === ConditionOperation.NEQ) { + const rightValue = condition.rightValue as boolean; + return leftValue !== rightValue; + } + if (operator === ConditionOperation.IS_TRUE) { + return leftValue === true; + } + if (operator === ConditionOperation.IS_FALSE) { + return leftValue === false; + } + if (operator === ConditionOperation.IN) { + const rightValue = condition.rightValue as boolean[]; + return rightValue.includes(leftValue); + } + if (operator === ConditionOperation.NIN) { + const rightValue = condition.rightValue as boolean[]; + return !rightValue.includes(leftValue); + } + if (operator === ConditionOperation.IS_EMPTY) { + return isNil(leftValue); + } + if (operator === ConditionOperation.IS_NOT_EMPTY) { + return !isNil(leftValue); + } + return false; +}; diff --git a/packages/runtime/js-core/src/nodes/condition/handlers/index.ts b/packages/runtime/js-core/src/nodes/condition/handlers/index.ts new file mode 100644 index 00000000..1467d955 --- /dev/null +++ b/packages/runtime/js-core/src/nodes/condition/handlers/index.ts @@ -0,0 +1,19 @@ +import { WorkflowVariableType } from '@flowgram.ai/runtime-interface'; + +import { ConditionHandlers } from '../type'; +import { conditionStringHandler } from './string'; +import { conditionObjectHandler } from './object'; +import { conditionNumberHandler } from './number'; +import { conditionNullHandler } from './null'; +import { conditionBooleanHandler } from './boolean'; +import { conditionArrayHandler } from './array'; + +export const conditionHandlers: ConditionHandlers = { + [WorkflowVariableType.String]: conditionStringHandler, + [WorkflowVariableType.Number]: conditionNumberHandler, + [WorkflowVariableType.Integer]: conditionNumberHandler, + [WorkflowVariableType.Boolean]: conditionBooleanHandler, + [WorkflowVariableType.Object]: conditionObjectHandler, + [WorkflowVariableType.Array]: conditionArrayHandler, + [WorkflowVariableType.Null]: conditionNullHandler, +}; diff --git a/packages/runtime/js-core/src/nodes/condition/handlers/null.ts b/packages/runtime/js-core/src/nodes/condition/handlers/null.ts new file mode 100644 index 00000000..ced17ef8 --- /dev/null +++ b/packages/runtime/js-core/src/nodes/condition/handlers/null.ts @@ -0,0 +1,19 @@ +import { isNil } from 'lodash-es'; + +import { ConditionHandler, ConditionOperation } from '../type'; + +export const conditionNullHandler: ConditionHandler = (condition) => { + const { operator } = condition; + const leftValue = condition.leftValue as unknown | null; + // Switch case share scope, so we need to use if else here + if (operator === ConditionOperation.EQ) { + return isNil(leftValue) && isNil(condition.rightValue); + } + if (operator === ConditionOperation.IS_EMPTY) { + return isNil(leftValue); + } + if (operator === ConditionOperation.IS_NOT_EMPTY) { + return !isNil(leftValue); + } + return false; +}; diff --git a/packages/runtime/js-core/src/nodes/condition/handlers/number.ts b/packages/runtime/js-core/src/nodes/condition/handlers/number.ts new file mode 100644 index 00000000..28697069 --- /dev/null +++ b/packages/runtime/js-core/src/nodes/condition/handlers/number.ts @@ -0,0 +1,48 @@ +import { isNil } from 'lodash-es'; + +import { ConditionHandler, ConditionOperation } from '../type'; + +export const conditionNumberHandler: ConditionHandler = (condition) => { + const { operator } = condition; + const leftValue = condition.leftValue as number; + // Switch case share scope, so we need to use if else here + if (operator === ConditionOperation.EQ) { + const rightValue = condition.rightValue as number; + return leftValue === rightValue; + } + if (operator === ConditionOperation.NEQ) { + const rightValue = condition.rightValue as number; + return leftValue !== rightValue; + } + if (operator === ConditionOperation.GT) { + const rightValue = condition.rightValue as number; + return leftValue > rightValue; + } + if (operator === ConditionOperation.GTE) { + const rightValue = condition.rightValue as number; + return leftValue >= rightValue; + } + if (operator === ConditionOperation.LT) { + const rightValue = condition.rightValue as number; + return leftValue < rightValue; + } + if (operator === ConditionOperation.LTE) { + const rightValue = condition.rightValue as number; + return leftValue <= rightValue; + } + if (operator === ConditionOperation.IN) { + const rightValue = condition.rightValue as number[]; + return rightValue.includes(leftValue); + } + if (operator === ConditionOperation.NIN) { + const rightValue = condition.rightValue as number[]; + return !rightValue.includes(leftValue); + } + if (operator === ConditionOperation.IS_EMPTY) { + return isNil(leftValue); + } + if (operator === ConditionOperation.IS_NOT_EMPTY) { + return !isNil(leftValue); + } + return false; +}; diff --git a/packages/runtime/js-core/src/nodes/condition/handlers/object.ts b/packages/runtime/js-core/src/nodes/condition/handlers/object.ts new file mode 100644 index 00000000..e7fd7a83 --- /dev/null +++ b/packages/runtime/js-core/src/nodes/condition/handlers/object.ts @@ -0,0 +1,16 @@ +import { isNil } from 'lodash-es'; + +import { ConditionHandler, ConditionOperation } from '../type'; + +export const conditionObjectHandler: ConditionHandler = (condition) => { + const { operator } = condition; + const leftValue = condition.leftValue as object; + // Switch case share scope, so we need to use if else here + if (operator === ConditionOperation.IS_EMPTY) { + return isNil(leftValue); + } + if (operator === ConditionOperation.IS_NOT_EMPTY) { + return !isNil(leftValue); + } + return false; +}; diff --git a/packages/runtime/js-core/src/nodes/condition/handlers/string.ts b/packages/runtime/js-core/src/nodes/condition/handlers/string.ts new file mode 100644 index 00000000..e5a78123 --- /dev/null +++ b/packages/runtime/js-core/src/nodes/condition/handlers/string.ts @@ -0,0 +1,40 @@ +import { isNil } from 'lodash-es'; + +import { ConditionHandler, ConditionOperation } from '../type'; + +export const conditionStringHandler: ConditionHandler = (condition) => { + const { operator } = condition; + const leftValue = condition.leftValue as string; + // Switch case share scope, so we need to use if else here + if (operator === ConditionOperation.EQ) { + const rightValue = condition.rightValue as string; + return leftValue === rightValue; + } + if (operator === ConditionOperation.NEQ) { + const rightValue = condition.rightValue as string; + return leftValue !== rightValue; + } + if (operator === ConditionOperation.CONTAINS) { + const rightValue = condition.rightValue as string; + return leftValue.includes(rightValue); + } + if (operator === ConditionOperation.NOT_CONTAINS) { + const rightValue = condition.rightValue as string; + return !leftValue.includes(rightValue); + } + if (operator === ConditionOperation.IN) { + const rightValue = condition.rightValue as string[]; + return rightValue.includes(leftValue); + } + if (operator === ConditionOperation.NIN) { + const rightValue = condition.rightValue as string[]; + return !rightValue.includes(leftValue); + } + if (operator === ConditionOperation.IS_EMPTY) { + return isNil(leftValue); + } + if (operator === ConditionOperation.IS_NOT_EMPTY) { + return !isNil(leftValue); + } + return false; +}; diff --git a/packages/runtime/js-core/src/nodes/condition/index.ts b/packages/runtime/js-core/src/nodes/condition/index.ts new file mode 100644 index 00000000..4be8156d --- /dev/null +++ b/packages/runtime/js-core/src/nodes/condition/index.ts @@ -0,0 +1,82 @@ +import { isNil } from 'lodash-es'; +import { + ExecutionContext, + ExecutionResult, + FlowGramNode, + INodeExecutor, + WorkflowVariableType, +} from '@flowgram.ai/runtime-interface'; + +import { ConditionItem, ConditionValue, Conditions } from './type'; +import { conditionRules } from './rules'; +import { conditionHandlers } from './handlers'; + +export class ConditionExecutor implements INodeExecutor { + public type = FlowGramNode.Condition; + + public async execute(context: ExecutionContext): Promise { + const conditions: Conditions = context.node.data?.conditions; + if (!conditions) { + return { + outputs: {}, + }; + } + const parsedConditions = conditions + .map((item) => this.parseCondition(item, context)) + .filter((item) => this.checkCondition(item)); + const activatedCondition = parsedConditions.find((item) => this.handleCondition(item)); + if (!activatedCondition) { + return { + outputs: {}, + }; + } + return { + outputs: {}, + branch: activatedCondition.key, + }; + } + + private parseCondition(item: ConditionItem, context: ExecutionContext): ConditionValue { + const { key, value } = item; + const { left, operator, right } = value; + const parsedLeft = context.runtime.state.parseRef(left); + const leftValue = parsedLeft?.value ?? null; + const leftType = parsedLeft?.type ?? WorkflowVariableType.Null; + const parsedRight = Boolean(right) ? context.runtime.state.parseValue(right) : null; + const rightValue = parsedRight?.value ?? null; + const rightType = parsedRight?.type ?? WorkflowVariableType.Null; + return { + key, + leftValue, + leftType, + rightValue, + rightType, + operator, + }; + } + + private checkCondition(condition: ConditionValue): boolean { + const rule = conditionRules[condition.leftType]; + if (isNil(rule)) { + throw new Error(`condition left type ${condition.leftType} is not supported`); + } + const ruleType = rule[condition.operator]; + if (isNil(ruleType)) { + throw new Error(`condition operator ${condition.operator} is not supported`); + } + if (ruleType !== condition.rightType) { + // throw new Error(`condition right type expected ${ruleType}, got ${condition.rightType}`); + return false; + } + return true; + } + + private handleCondition(condition: ConditionValue): boolean { + const handler = conditionHandlers[condition.leftType]; + if (!handler) { + throw new Error(`condition left type ${condition.leftType} is not supported`); + } + const isActive = handler(condition); + return isActive; + } +} diff --git a/packages/runtime/js-core/src/nodes/condition/rules.ts b/packages/runtime/js-core/src/nodes/condition/rules.ts new file mode 100644 index 00000000..5c4740dc --- /dev/null +++ b/packages/runtime/js-core/src/nodes/condition/rules.ts @@ -0,0 +1,63 @@ +import { WorkflowVariableType } from '@flowgram.ai/runtime-interface'; + +import { ConditionOperation, ConditionRules } from './type'; + +export const conditionRules: ConditionRules = { + [WorkflowVariableType.String]: { + [ConditionOperation.EQ]: WorkflowVariableType.String, + [ConditionOperation.NEQ]: WorkflowVariableType.String, + [ConditionOperation.CONTAINS]: WorkflowVariableType.String, + [ConditionOperation.NOT_CONTAINS]: WorkflowVariableType.String, + [ConditionOperation.IN]: WorkflowVariableType.Array, + [ConditionOperation.NIN]: WorkflowVariableType.Array, + [ConditionOperation.IS_EMPTY]: WorkflowVariableType.String, + [ConditionOperation.IS_NOT_EMPTY]: WorkflowVariableType.String, + }, + [WorkflowVariableType.Number]: { + [ConditionOperation.EQ]: WorkflowVariableType.Number, + [ConditionOperation.NEQ]: WorkflowVariableType.Number, + [ConditionOperation.GT]: WorkflowVariableType.Number, + [ConditionOperation.GTE]: WorkflowVariableType.Number, + [ConditionOperation.LT]: WorkflowVariableType.Number, + [ConditionOperation.LTE]: WorkflowVariableType.Number, + [ConditionOperation.IN]: WorkflowVariableType.Array, + [ConditionOperation.NIN]: WorkflowVariableType.Array, + [ConditionOperation.IS_EMPTY]: WorkflowVariableType.Null, + [ConditionOperation.IS_NOT_EMPTY]: WorkflowVariableType.Null, + }, + [WorkflowVariableType.Integer]: { + [ConditionOperation.EQ]: WorkflowVariableType.Integer, + [ConditionOperation.NEQ]: WorkflowVariableType.Integer, + [ConditionOperation.GT]: WorkflowVariableType.Integer, + [ConditionOperation.GTE]: WorkflowVariableType.Integer, + [ConditionOperation.LT]: WorkflowVariableType.Integer, + [ConditionOperation.LTE]: WorkflowVariableType.Integer, + [ConditionOperation.IN]: WorkflowVariableType.Array, + [ConditionOperation.NIN]: WorkflowVariableType.Array, + [ConditionOperation.IS_EMPTY]: WorkflowVariableType.Null, + [ConditionOperation.IS_NOT_EMPTY]: WorkflowVariableType.Null, + }, + [WorkflowVariableType.Boolean]: { + [ConditionOperation.EQ]: WorkflowVariableType.Boolean, + [ConditionOperation.NEQ]: WorkflowVariableType.Boolean, + [ConditionOperation.IS_TRUE]: WorkflowVariableType.Null, + [ConditionOperation.IS_FALSE]: WorkflowVariableType.Null, + [ConditionOperation.IN]: WorkflowVariableType.Array, + [ConditionOperation.NIN]: WorkflowVariableType.Array, + [ConditionOperation.IS_EMPTY]: WorkflowVariableType.Null, + [ConditionOperation.IS_NOT_EMPTY]: WorkflowVariableType.Null, + }, + [WorkflowVariableType.Object]: { + [ConditionOperation.IS_EMPTY]: WorkflowVariableType.Null, + [ConditionOperation.IS_NOT_EMPTY]: WorkflowVariableType.Null, + }, + [WorkflowVariableType.Array]: { + [ConditionOperation.IS_EMPTY]: WorkflowVariableType.Null, + [ConditionOperation.IS_NOT_EMPTY]: WorkflowVariableType.Null, + }, + [WorkflowVariableType.Null]: { + [ConditionOperation.EQ]: WorkflowVariableType.Null, + [ConditionOperation.IS_EMPTY]: WorkflowVariableType.Null, + [ConditionOperation.IS_NOT_EMPTY]: WorkflowVariableType.Null, + }, +}; diff --git a/packages/runtime/js-core/src/nodes/condition/type.ts b/packages/runtime/js-core/src/nodes/condition/type.ts new file mode 100644 index 00000000..22659830 --- /dev/null +++ b/packages/runtime/js-core/src/nodes/condition/type.ts @@ -0,0 +1,50 @@ +import { + WorkflowVariableType, + IFlowConstantRefValue, + IFlowRefValue, +} from '@flowgram.ai/runtime-interface'; + +export enum ConditionOperation { + EQ = 'eq', + NEQ = 'neq', + GT = 'gt', + GTE = 'gte', + LT = 'lt', + LTE = 'lte', + IN = 'in', + NIN = 'nin', + CONTAINS = 'contains', + NOT_CONTAINS = 'not_contains', + IS_EMPTY = 'is_empty', + IS_NOT_EMPTY = 'is_not_empty', + IS_TRUE = 'is_true', + IS_FALSE = 'is_false', +} + +export interface ConditionItem { + key: string; + value: { + left: IFlowRefValue; + operator: ConditionOperation; + right: IFlowConstantRefValue; + }; +} + +export type Conditions = ConditionItem[]; + +export type ConditionRule = Partial>; + +export type ConditionRules = Record; + +export interface ConditionValue { + key: string; + leftValue: unknown | null; + rightValue: unknown | null; + leftType: WorkflowVariableType; + rightType: WorkflowVariableType; + operator: ConditionOperation; +} + +export type ConditionHandler = (condition: ConditionValue) => boolean; + +export type ConditionHandlers = Record; diff --git a/packages/runtime/js-core/src/nodes/end/index.ts b/packages/runtime/js-core/src/nodes/end/index.ts new file mode 100644 index 00000000..c6ad6551 --- /dev/null +++ b/packages/runtime/js-core/src/nodes/end/index.ts @@ -0,0 +1,17 @@ +import { + ExecutionContext, + ExecutionResult, + FlowGramNode, + INodeExecutor, +} from '@flowgram.ai/runtime-interface'; + +export class EndExecutor implements INodeExecutor { + public type = FlowGramNode.End; + + public async execute(context: ExecutionContext): Promise { + context.runtime.ioCenter.setOutputs(context.inputs); + return { + outputs: context.inputs, + }; + } +} diff --git a/packages/runtime/js-core/src/nodes/index.ts b/packages/runtime/js-core/src/nodes/index.ts new file mode 100644 index 00000000..c953671f --- /dev/null +++ b/packages/runtime/js-core/src/nodes/index.ts @@ -0,0 +1,15 @@ +import { INodeExecutorFactory } from '@flowgram.ai/runtime-interface'; + +import { StartExecutor } from './start'; +import { LoopExecutor } from './loop'; +import { LLMExecutor } from './llm'; +import { EndExecutor } from './end'; +import { ConditionExecutor } from './condition'; + +export const WorkflowRuntimeNodeExecutors: INodeExecutorFactory[] = [ + StartExecutor, + EndExecutor, + LLMExecutor, + ConditionExecutor, + LoopExecutor, +]; diff --git a/packages/runtime/js-core/src/nodes/llm/index.ts b/packages/runtime/js-core/src/nodes/llm/index.ts new file mode 100644 index 00000000..4ce55d94 --- /dev/null +++ b/packages/runtime/js-core/src/nodes/llm/index.ts @@ -0,0 +1,69 @@ +import { isNil } from 'lodash-es'; +import { ChatOpenAI } from '@langchain/openai'; +import { SystemMessage, HumanMessage, BaseMessageLike } from '@langchain/core/messages'; +import { + ExecutionContext, + ExecutionResult, + FlowGramNode, + INodeExecutor, +} from '@flowgram.ai/runtime-interface'; + +export interface LLMExecutorInputs { + modelName: string; + apiKey: string; + apiHost: string; + temperature: number; + systemPrompt?: string; + prompt: string; +} + +export class LLMExecutor implements INodeExecutor { + public type = FlowGramNode.LLM; + + public async execute(context: ExecutionContext): Promise { + const inputs = context.inputs as LLMExecutorInputs; + this.checkInputs(inputs); + + const { modelName, temperature, apiKey, apiHost, systemPrompt, prompt } = inputs; + + const model = new ChatOpenAI({ + modelName, + temperature, + apiKey, + configuration: { + baseURL: apiHost, + }, + }); + + const messages: BaseMessageLike[] = []; + + if (systemPrompt) { + messages.push(new SystemMessage(systemPrompt)); + } + messages.push(new HumanMessage(prompt)); + + const apiMessage = await model.invoke(messages); + + const result = apiMessage.content; + return { + outputs: { + result, + }, + }; + } + + protected checkInputs(inputs: LLMExecutorInputs) { + const { modelName, temperature, apiKey, apiHost, prompt } = inputs; + const missingInputs = []; + + if (isNil(modelName)) missingInputs.push('modelName'); + if (isNil(temperature)) missingInputs.push('temperature'); + if (isNil(apiKey)) missingInputs.push('apiKey'); + if (isNil(apiHost)) missingInputs.push('apiHost'); + if (isNil(prompt)) missingInputs.push('prompt'); + + if (missingInputs.length > 0) { + throw new Error(`LLM node missing required inputs: ${missingInputs.join(', ')}`); + } + } +} diff --git a/packages/runtime/js-core/src/nodes/loop/index.ts b/packages/runtime/js-core/src/nodes/loop/index.ts new file mode 100644 index 00000000..13f76752 --- /dev/null +++ b/packages/runtime/js-core/src/nodes/loop/index.ts @@ -0,0 +1,77 @@ +import { isNil } from 'lodash-es'; +import { + ExecutionContext, + ExecutionResult, + FlowGramNode, + IEngine, + INodeExecutor, + IVariableParseResult, + WorkflowVariableType, +} from '@flowgram.ai/runtime-interface'; + +type LoopArray = Array; + +export interface LoopExecutorInputs { + batchFor: LoopArray; +} + +export class LoopExecutor implements INodeExecutor { + public type = FlowGramNode.Loop; + + public async execute(context: ExecutionContext): Promise { + const loopNodeID = context.node.id; + const loopArrayResult = context.runtime.state.parseRef(context.node.data.batchFor)!; + this.checkLoopArray(loopArrayResult); + + const loopArray = loopArrayResult.value; + const itemsType = loopArrayResult.itemsType!; + const engine = context.container.get(IEngine); + const subNodes = context.node.children; + const startSubNodes = subNodes.filter((node) => node.prev.length === 0); + + if (loopArray.length === 0 || startSubNodes.length === 0) { + return { + outputs: {}, + }; + } + + // not use Array method to make error stack more concise, and better performance + for (let i = 0; i < loopArray.length; i++) { + const loopItem = loopArray[i]; + const subContext = context.runtime.sub(); + subContext.variableStore.setVariable({ + nodeID: `${loopNodeID}_locals`, + key: 'item', + type: itemsType, + value: loopItem, + }); + await Promise.all( + startSubNodes.map((node) => + engine.executeNode({ + context: subContext, + node, + }) + ) + ); + } + + return { + outputs: {}, + }; + } + + private checkLoopArray(loopArrayResult: IVariableParseResult | null): void { + const loopArray = loopArrayResult?.value; + if (!loopArray || isNil(loopArray) || !Array.isArray(loopArray)) { + throw new Error('batchFor is required'); + } + const loopArrayType = loopArrayResult.type; + if (loopArrayType !== WorkflowVariableType.Array) { + throw new Error('batchFor must be an array'); + } + const loopArrayItemType = loopArrayResult.itemsType; + if (isNil(loopArrayItemType)) { + throw new Error('batchFor items must be array items'); + } + } +} diff --git a/packages/runtime/js-core/src/nodes/start/index.ts b/packages/runtime/js-core/src/nodes/start/index.ts new file mode 100644 index 00000000..b51de1d8 --- /dev/null +++ b/packages/runtime/js-core/src/nodes/start/index.ts @@ -0,0 +1,16 @@ +import { + ExecutionContext, + ExecutionResult, + FlowGramNode, + INodeExecutor, +} from '@flowgram.ai/runtime-interface'; + +export class StartExecutor implements INodeExecutor { + public type = FlowGramNode.Start; + + public async execute(context: ExecutionContext): Promise { + return { + outputs: context.runtime.ioCenter.inputs, + }; + } +} diff --git a/packages/runtime/js-core/tsconfig.json b/packages/runtime/js-core/tsconfig.json new file mode 100644 index 00000000..4f6dd941 --- /dev/null +++ b/packages/runtime/js-core/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json", + "compilerOptions": { + "baseUrl": "src", + "paths": { + "@application/*": [ + "application/*" + ], + "@workflow/*": [ + "domain/*" + ], + "@infra/*": [ + "infrastructure/*" + ], + "@nodes/*": [ + "nodes/*" + ], + } + }, + "include": [ + "./src" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/runtime/js-core/vitest.config.ts b/packages/runtime/js-core/vitest.config.ts new file mode 100644 index 00000000..e4e2c35b --- /dev/null +++ b/packages/runtime/js-core/vitest.config.ts @@ -0,0 +1,39 @@ +import { config } from "dotenv"; +import path from 'path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + build: { + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + resolve: { + alias: [ + {find: "@application", replacement: path.resolve(__dirname, './src/application') }, + {find: "@workflow", replacement: path.resolve(__dirname, './src/domain') }, + {find: "@infra", replacement: path.resolve(__dirname, './src/infrastructure') }, + {find: "@nodes", replacement: path.resolve(__dirname, './src/nodes') }, + ], + }, + test: { + globals: true, + mockReset: false, + environment: 'jsdom', + testTimeout: 15000, + setupFiles: [path.resolve(__dirname, './src/domain/__tests__/setup.ts')], + include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'], + exclude: [ + '**/__mocks__**', + '**/node_modules/**', + '**/dist/**', + '**/lib/**', // lib 编译结果忽略掉 + '**/cypress/**', + '**/.{idea,git,cache,output,temp}/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', + ], + env: { + ...config({ path: path.resolve(__dirname, './.env/.env.test') }).parsed + } + }, +}); diff --git a/packages/runtime/nodejs/.eslintignore b/packages/runtime/nodejs/.eslintignore new file mode 100644 index 00000000..cf64a52f --- /dev/null +++ b/packages/runtime/nodejs/.eslintignore @@ -0,0 +1 @@ +.eslintrc.cjs diff --git a/packages/runtime/nodejs/.eslintrc.cjs b/packages/runtime/nodejs/.eslintrc.cjs new file mode 100644 index 00000000..9c2f4ba5 --- /dev/null +++ b/packages/runtime/nodejs/.eslintrc.cjs @@ -0,0 +1,24 @@ +const { defineConfig } = require('@flowgram.ai/eslint-config'); + +module.exports = defineConfig({ + parser: '@typescript-eslint/parser', + preset: 'node', + packageRoot: __dirname, + parserOptions: { + requireConfigFile: false, + ecmaVersion: 2017, + sourceType: "module", + ecmaFeatures: { + modules: true, + } + }, + rules: { + 'no-console': 'off', + }, + plugins: ['json', '@typescript-eslint'], + settings: { + react: { + version: '18', + }, + }, +}); diff --git a/packages/runtime/nodejs/.gitignore b/packages/runtime/nodejs/.gitignore new file mode 100644 index 00000000..5ef6a520 --- /dev/null +++ b/packages/runtime/nodejs/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/packages/runtime/nodejs/README.md b/packages/runtime/nodejs/README.md new file mode 100644 index 00000000..d7615d89 --- /dev/null +++ b/packages/runtime/nodejs/README.md @@ -0,0 +1,16 @@ +# FlowGram Runtime NodeJS + +## Starting the server +```bash +pnpm dev +``` + +## Building the server +```bash +pnpm build +``` + +## Running the server +```bash +pnpm start +``` diff --git a/packages/runtime/nodejs/package.json b/packages/runtime/nodejs/package.json new file mode 100644 index 00000000..3e5571ae --- /dev/null +++ b/packages/runtime/nodejs/package.json @@ -0,0 +1,62 @@ +{ + "name": "@flowgram.ai/runtime-nodejs", + "version": "0.1.0", + "description": "", + "keywords": [], + "license": "MIT", + "scripts": { + "dev": "tsx watch src", + "build": "npm run build:fast -- --dts-resolve", + "build:fast": "tsup src/index.ts --format cjs,esm --sourcemap --legacy-output", + "build:watch": "npm run build:fast -- --dts-resolve", + "lint": "eslint --cache src", + "lint-fix": "eslint --fix src", + "type-check": "tsc", + "start": "node dist/index.js", + "test": "exit 0", + "test:cov": "exit 0" + }, + "dependencies": { + "@flowgram.ai/runtime-js": "workspace:*", + "@flowgram.ai/runtime-interface": "workspace:*", + "@fastify/cors": "^8.2.1", + "@fastify/swagger": "^8.5.1", + "@fastify/swagger-ui": "4.1.0", + "@langchain/openai": "^0.5.11", + "@langchain/core": "^0.3.57", + "@fastify/websocket": "^10.0.1", + "@trpc/server": "^10.27.1", + "trpc-openapi": "^1.2.0", + "fastify": "^4.17.0", + "tslib": "^2.8.1", + "lodash-es": "^4.17.21", + "ws": "^8.0.0", + "zod": "^3.24.4" + }, + "devDependencies": { + "@flowgram.ai/ts-config": "workspace:*", + "@flowgram.ai/eslint-config": "workspace:*", + "@types/cors": "^2.8.13", + "dotenv": "~16.5.0", + "@types/node": "^18", + "@types/ws": "^8.2.0", + "@types/lodash-es": "^4.17.12", + "eslint": "^8.54.0", + "npm-run-all": "^4.1.5", + "@babel/eslint-parser": "~7.19.1", + "typescript": "^5.0.4", + "tsup": "^8.0.1", + "tsx": "~4.19.4", + "eslint-plugin-json": "^4.0.1", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", + "@eslint/eslintrc": "^3", + "@vitest/coverage-v8": "^0.32.0", + "vitest": "^0.34.6", + "wait-port": "^1.0.1" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/runtime/nodejs/src/api/create-api.ts b/packages/runtime/nodejs/src/api/create-api.ts new file mode 100644 index 00000000..1c2abefc --- /dev/null +++ b/packages/runtime/nodejs/src/api/create-api.ts @@ -0,0 +1,63 @@ +import z from 'zod'; +import { WorkflowRuntimeAPIs } from '@flowgram.ai/runtime-js'; +import { FlowGramAPIMethod, FlowGramAPIName, FlowGramAPIs } from '@flowgram.ai/runtime-interface'; + +import { APIHandler } from './type'; +import { publicProcedure } from './trpc'; + +export const createAPI = (apiName: FlowGramAPIName): APIHandler => { + const define = FlowGramAPIs[apiName]; + const caller = WorkflowRuntimeAPIs[apiName]; + if (define.method === FlowGramAPIMethod.GET) { + const procedure = publicProcedure + .meta({ + openapi: { + method: define.method, + path: define.path, + summary: define.name, + tags: [define.module], + }, + }) + .input(define.schema.input) + .output(z.union([define.schema.output, z.undefined()])) + .query(async (opts) => { + const input = opts.input; + try { + const output = await caller(input); + return output; + } catch { + return undefined; + } + }); + + return { + define, + procedure: procedure as any, + }; + } + + const procedure = publicProcedure + .meta({ + openapi: { + method: define.method, + path: define.path, + summary: define.name, + tags: [define.module], + }, + }) + .input(define.schema.input) + .output(z.union([define.schema.output, z.undefined()])) + .mutation(async (opts) => { + const input = opts.input; + try { + const output = await caller(input); + return output; + } catch { + return undefined; + } + }); + return { + define, + procedure: procedure as any, + }; +}; diff --git a/packages/runtime/nodejs/src/api/index.ts b/packages/runtime/nodejs/src/api/index.ts new file mode 100644 index 00000000..ec640ef6 --- /dev/null +++ b/packages/runtime/nodejs/src/api/index.ts @@ -0,0 +1,16 @@ +import { FlowGramAPINames } from '@flowgram.ai/runtime-interface'; + +import { APIRouter } from './type'; +import { router } from './trpc'; +import { createAPI } from './create-api'; + +const APIS = FlowGramAPINames.map((apiName) => createAPI(apiName)); + +export const routers = APIS.reduce((acc, api) => { + acc[api.define.path] = api.procedure; + return acc; +}, {} as APIRouter); + +export const appRouter = router(routers); + +export type AppRouter = typeof appRouter; diff --git a/packages/runtime/nodejs/src/api/trpc.ts b/packages/runtime/nodejs/src/api/trpc.ts new file mode 100644 index 00000000..3a62ba7c --- /dev/null +++ b/packages/runtime/nodejs/src/api/trpc.ts @@ -0,0 +1,16 @@ +import { OpenApiMeta } from 'trpc-openapi'; +import { initTRPC } from '@trpc/server'; + +import type { Context } from '../server/context'; + +const t = initTRPC + .context() + .meta() + .create({ + errorFormatter({ shape }) { + return shape; + }, + }); + +export const router = t.router; +export const publicProcedure = t.procedure; diff --git a/packages/runtime/nodejs/src/api/type.ts b/packages/runtime/nodejs/src/api/type.ts new file mode 100644 index 00000000..0f9aa56d --- /dev/null +++ b/packages/runtime/nodejs/src/api/type.ts @@ -0,0 +1,9 @@ +import { BuildProcedure } from '@trpc/server'; +import { FlowGramAPIDefine } from '@flowgram.ai/runtime-interface'; + +export interface APIHandler { + define: FlowGramAPIDefine; + procedure: BuildProcedure; +} + +export type APIRouter = Record; diff --git a/packages/runtime/nodejs/src/config/index.ts b/packages/runtime/nodejs/src/config/index.ts new file mode 100644 index 00000000..9e50faa2 --- /dev/null +++ b/packages/runtime/nodejs/src/config/index.ts @@ -0,0 +1,13 @@ +import type { ServerParams } from '@server/type'; + +export const ServerConfig: ServerParams = { + name: 'flowgram-runtime', + title: 'FlowGram Runtime', + description: 'FlowGram Runtime Demo', + runtime: 'nodejs', + version: '0.0.1', + dev: false, + port: 4000, + basePath: '/api', + docsPath: '/docs', +}; diff --git a/packages/runtime/nodejs/src/index.ts b/packages/runtime/nodejs/src/index.ts new file mode 100644 index 00000000..ce697545 --- /dev/null +++ b/packages/runtime/nodejs/src/index.ts @@ -0,0 +1,8 @@ +import { createServer } from '@server/index'; + +async function main() { + const server = await createServer(); + server.start(); +} + +main(); diff --git a/packages/runtime/nodejs/src/server/context.ts b/packages/runtime/nodejs/src/server/context.ts new file mode 100644 index 00000000..20978063 --- /dev/null +++ b/packages/runtime/nodejs/src/server/context.ts @@ -0,0 +1,8 @@ +import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify'; + +export function createContext(ctx: CreateFastifyContextOptions) { + const { req, res } = ctx; + return { req, res }; +} + +export type Context = Awaited>; diff --git a/packages/runtime/nodejs/src/server/docs.ts b/packages/runtime/nodejs/src/server/docs.ts new file mode 100644 index 00000000..3748b1b7 --- /dev/null +++ b/packages/runtime/nodejs/src/server/docs.ts @@ -0,0 +1,14 @@ +import { generateOpenApiDocument } from 'trpc-openapi'; + +import { ServerConfig } from '@config/index'; +import { appRouter } from '@api/index'; + +// Generate OpenAPI schema document +export const serverDocument = generateOpenApiDocument(appRouter, { + title: ServerConfig.title, + description: ServerConfig.description, + version: ServerConfig.version, + baseUrl: `http://localhost:${ServerConfig.port}${ServerConfig.basePath}`, + docsUrl: 'https://flowgram.ai', + tags: ['Task'], +}); diff --git a/packages/runtime/nodejs/src/server/index.ts b/packages/runtime/nodejs/src/server/index.ts new file mode 100644 index 00000000..3eba1b97 --- /dev/null +++ b/packages/runtime/nodejs/src/server/index.ts @@ -0,0 +1,89 @@ +import { fastifyTRPCOpenApiPlugin } from 'trpc-openapi'; +import fastify from 'fastify'; +import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify'; +import { ServerInfoDefine, type ServerInfoOutput } from '@flowgram.ai/runtime-interface'; +import ws from '@fastify/websocket'; +import fastifySwaggerUI from '@fastify/swagger-ui'; +import fastifySwagger from '@fastify/swagger'; +import cors from '@fastify/cors'; + +import { ServerConfig } from '@config/index'; +import { appRouter } from '@api/index'; +import { serverDocument } from './docs'; +import { createContext } from './context'; + +export async function createServer() { + const server = fastify({ logger: ServerConfig.dev }); + + await server.register(cors); + await server.register(ws); + await server.register(fastifyTRPCPlugin, { + prefix: '/trpc', + useWss: false, + trpcOptions: { router: appRouter, createContext }, + }); + await server.register(fastifyTRPCOpenApiPlugin, { + basePath: ServerConfig.basePath, + router: appRouter, + createContext, + } as any); + + await server.register(fastifySwagger, { + mode: 'static', + specification: { document: serverDocument }, + uiConfig: { displayOperationId: true }, + exposeRoute: true, + } as any); + + await server.register(fastifySwaggerUI, { + routePrefix: ServerConfig.docsPath, + uiConfig: { + docExpansion: 'full', + deepLinking: false, + }, + uiHooks: { + onRequest: function (request, reply, next) { + next(); + }, + preHandler: function (request, reply, next) { + next(); + }, + }, + staticCSP: true, + transformStaticCSP: (header) => header, + transformSpecification: (swaggerObject, request, reply) => swaggerObject, + transformSpecificationClone: true, + }); + + server.get(ServerInfoDefine.path, async (): Promise => { + const serverTime = new Date(); + const output: ServerInfoOutput = { + name: ServerConfig.name, + title: ServerConfig.title, + description: ServerConfig.description, + runtime: ServerConfig.runtime, + version: ServerConfig.version, + time: serverTime.toISOString(), + }; + return output; + }); + + const stop = async () => { + await server.close(); + }; + const start = async () => { + try { + const address = await server.listen({ port: ServerConfig.port }); + await server.ready(); + server.swagger(); + console.log( + `> Listen Port: ${ServerConfig.port}\n> Server Address: ${address}\n> API Docs: http://localhost:4000/docs` + ); + } catch (err) { + server.log.error(err); + process.exit(1); + } + }; + + return { server, start, stop }; +} diff --git a/packages/runtime/nodejs/src/server/type.ts b/packages/runtime/nodejs/src/server/type.ts new file mode 100644 index 00000000..c40f8cb8 --- /dev/null +++ b/packages/runtime/nodejs/src/server/type.ts @@ -0,0 +1,8 @@ +import { ServerInfoOutput } from '@flowgram.ai/runtime-interface'; + +export interface ServerParams extends Omit { + dev: boolean; + port: number; + basePath: string; + docsPath: string; +} diff --git a/packages/runtime/nodejs/tsconfig.json b/packages/runtime/nodejs/tsconfig.json new file mode 100644 index 00000000..26bd95ea --- /dev/null +++ b/packages/runtime/nodejs/tsconfig.json @@ -0,0 +1,48 @@ +{ + "extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json", + "compilerOptions": { + "target": "esnext", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [], + "baseUrl": "src", + "paths": { + "@api/*": [ + "api/*" + ], + "@application/*": [ + "application/*" + ], + "@server/*": [ + "server/*" + ], + "@config/*": [ + "config/*" + ], + "@workflow/*": [ + "workflow/*" + ] + } + }, + "include": [ + "**/*.ts", + "**/*.tsx", + "src/workflow/executor/condition/constant.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/runtime/nodejs/vitest.config.ts b/packages/runtime/nodejs/vitest.config.ts new file mode 100644 index 00000000..1e8c3611 --- /dev/null +++ b/packages/runtime/nodejs/vitest.config.ts @@ -0,0 +1,40 @@ +import { config } from "dotenv"; +import path from 'path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + build: { + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + resolve: { + alias: [ + {find: "@api", replacement: path.resolve(__dirname, './src/api') }, + {find: "@application", replacement: path.resolve(__dirname, './src/application') }, + {find: "@server", replacement: path.resolve(__dirname, './src/server') }, + {find: "@config", replacement: path.resolve(__dirname, './src/config') }, + {find: "@workflow", replacement: path.resolve(__dirname, './src/workflow') }, + ], + }, + test: { + globals: true, + mockReset: false, + environment: 'jsdom', + testTimeout: 15000, + setupFiles: [path.resolve(__dirname, './src/workflow/__tests__/setup.ts')], + include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'], + exclude: [ + '**/__mocks__**', + '**/node_modules/**', + '**/dist/**', + '**/lib/**', // lib 编译结果忽略掉 + '**/cypress/**', + '**/.{idea,git,cache,output,temp}/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', + ], + env: { + ...config({ path: path.resolve(__dirname, './.env/.env.test') }).parsed + } + }, +}); diff --git a/rush.json b/rush.json index d55c3431..7d6641a1 100644 --- a/rush.json +++ b/rush.json @@ -4,7 +4,6 @@ */ { "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", - /** * (Required) This specifies the version of the Rush engine to be used in this repo. * Rush's "version selector" feature ensures that the globally installed tool will @@ -17,7 +16,6 @@ * correct error-underlining and tab-completion for editors such as VS Code. */ "rushVersion": "5.140.1", - /** * The next field selects which package manager should be installed and determines its version. * Rush installs its own local copy of the package manager to ensure that your build process @@ -27,10 +25,8 @@ * for details about these alternatives. */ "pnpmVersion": "8.15.8", - // "npmVersion": "6.14.15", // "yarnVersion": "1.9.4", - /** * Older releases of the Node.js engine may be missing features required by your system. * Other releases may have bugs. In particular, the "latest" version will not be a @@ -43,7 +39,6 @@ * LTS versions: https://nodejs.org/en/download/releases/ */ "nodeSupportedVersionRange": ">=18.20.3 <19.0.0 || >=20.14.0 <23.0.0", - /** * If the version check above fails, Rush will display a message showing the current * node version and the supported version range. You can use this setting to provide @@ -51,7 +46,6 @@ * tool or script you'd like the user to use to get in line with the expected version. */ // "nodeSupportedVersionInstructions": "Run 'nvs use' to switch to the expected node version.", - /** * Odd-numbered major versions of Node.js are experimental. Even-numbered releases * spend six months in a stabilization period before the first Long Term Support (LTS) version. @@ -64,14 +58,12 @@ * to disable Rush's warning. */ // "suppressNodeLtsWarning": false, - /** * Rush normally prints a warning if it detects that the current version is not one published to the * public npmjs.org registry. If you need to block calls to the npm registry, you can use this setting to disable * Rush's check. */ // "suppressRushIsPublicVersionCheck": false, - /** * Large monorepos can become intimidating for newcomers if project folder paths don't follow * a consistent and recognizable pattern. When the system allows nested folder trees, @@ -97,7 +89,6 @@ */ "projectFolderMinDepth": 2, "projectFolderMaxDepth": 4, - /** * Today the npmjs.com registry enforces fairly strict naming rules for packages, but in the early * days there was no standard and hardly any enforcement. A few large legacy projects are still using @@ -110,7 +101,6 @@ * The default value is false. */ // "allowMostlyStandardPackageNames": true, - /** * This feature helps you to review and approve new packages before they are introduced * to your monorepo. For example, you may be concerned about licensing, code quality, @@ -146,7 +136,6 @@ // */ // // "ignoredNpmScopes": ["@types"] // }, - /** * If you use Git as your version control system, this section has some additional * optional features you can use. @@ -196,7 +185,6 @@ */ // "changefilesCommitMessage": "Rush change" }, - "repository": { /** * The URL of this Git repository, used by "rush change" to determine the base branch for your PR. @@ -225,7 +213,6 @@ */ // "defaultRemote": "origin" }, - /** * Event hooks are customized script actions that Rush executes when specific events occur */ @@ -236,33 +223,27 @@ "preRushInstall": [ // "common/scripts/pre-rush-install.js" ], - /** * A list of shell commands to run after "rush install" or "rush update" finishes installation */ "postRushInstall": [], - /** * A list of shell commands to run before "rush build" or "rush rebuild" starts building */ "preRushBuild": [], - /** * A list of shell commands to run after "rush build" or "rush rebuild" finishes building */ "postRushBuild": [], - /** * A list of shell commands to run before the "rushx" command starts */ "preRushx": [], - /** * A list of shell commands to run after the "rushx" command finishes */ "postRushx": [] }, - /** * Installation variants allow you to maintain a parallel set of configuration files that can be * used to build the entire monorepo with an alternate set of dependencies. For example, suppose @@ -293,7 +274,6 @@ // "description": "Build this repo using the previous release of the SDK" // } ], - /** * Rush can collect anonymous telemetry about everyday developer activity such as * success/failure of installs, builds, and other operations. You can use this to identify @@ -303,14 +283,12 @@ * in the "eventHooks" section. */ // "telemetryEnabled": false, - /** * Allows creation of hotfix changes. This feature is experimental so it is disabled by default. * If this is set, 'rush change' only allows a 'hotfix' change type to be specified. This change type * will be used when publishing subsequent changes from the monorepo. */ // "hotfixChangeEnabled": false, - /** * This is an optional, but recommended, list of allowed tags that can be applied to Rush projects * using the "tags" setting in this file. This list is useful for preventing mistakes such as misspelling, @@ -320,7 +298,6 @@ * ".", and "@" characters. */ // "allowedProjectTags": [ "tools", "frontend-team", "1.0.0-release" ], - /** * (Required) This is the inventory of projects to be managed by Rush. * @@ -431,368 +408,581 @@ { "packageName": "@flowgram.ai/e2e-fixed-layout", "projectFolder": "e2e/fixed-layout", - "tags": ["e2e"] + "tags": [ + "e2e" + ] }, { "packageName": "@flowgram.ai/e2e-free-layout", "projectFolder": "e2e/free-layout", - "tags": ["e2e"] + "tags": [ + "e2e" + ] }, // eslint 通用配置 { "packageName": "@flowgram.ai/eslint-config", "projectFolder": "config/eslint-config", "versionPolicyName": "publishPolicy", - "tags": ["config"] + "tags": [ + "config" + ] }, // ts 通用配置 { "packageName": "@flowgram.ai/ts-config", "projectFolder": "config/ts-config", "versionPolicyName": "publishPolicy", - "tags": ["config"] + "tags": [ + "config" + ] }, { "packageName": "@flowgram.ai/create-app", "projectFolder": "apps/create-app", "versionPolicyName": "publishPolicy", - "tags": ["cli"] + "tags": [ + "cli" + ] }, // 官网 { "packageName": "@flowgram.ai/docs", "projectFolder": "apps/docs", - "tags": ["docs"] + "tags": [ + "docs" + ] }, // demos { "packageName": "@flowgram.ai/demo-fixed-layout", "projectFolder": "apps/demo-fixed-layout", - "tags": ["level-1", "team-flow", "demo"], + "tags": [ + "level-1", + "team-flow", + "demo" + ], "versionPolicyName": "appPolicy" }, { "packageName": "@flowgram.ai/utils", "projectFolder": "packages/common/utils", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/core", "projectFolder": "packages/canvas-engine/core", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/document", "projectFolder": "packages/canvas-engine/document", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/renderer", "projectFolder": "packages/canvas-engine/renderer", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/reactive", "projectFolder": "packages/common/reactive", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/background-plugin", "projectFolder": "packages/plugins/background-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/fixed-layout-core", "projectFolder": "packages/canvas-engine/fixed-layout-core", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-layout-core", "projectFolder": "packages/canvas-engine/free-layout-core", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/editor", "projectFolder": "packages/client/editor", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/fixed-drag-plugin", "projectFolder": "packages/plugins/fixed-drag-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/fixed-history-plugin", "projectFolder": "packages/plugins/fixed-history-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/fixed-layout-editor", "projectFolder": "packages/client/fixed-layout-editor", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/fixed-reactor-plugin", "projectFolder": "packages/plugins/fixed-reactor-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/form", "projectFolder": "packages/node-engine/form", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/form-core", "projectFolder": "packages/node-engine/form-core", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-history-plugin", "projectFolder": "packages/plugins/free-history-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-hover-plugin", "projectFolder": "packages/plugins/free-hover-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-layout-editor", "projectFolder": "packages/client/free-layout-editor", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-lines-plugin", "projectFolder": "packages/plugins/free-lines-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-node-panel-plugin", "projectFolder": "packages/plugins/free-node-panel-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-snap-plugin", "projectFolder": "packages/plugins/free-snap-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-stack-plugin", "projectFolder": "packages/plugins/free-stack-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-container-plugin", "projectFolder": "packages/plugins/free-container-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-group-plugin", "projectFolder": "packages/plugins/free-group-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/group-plugin", "projectFolder": "packages/plugins/group-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/history-node-plugin", "projectFolder": "packages/plugins/history-node-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/minimap-plugin", "projectFolder": "packages/plugins/minimap-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/node", "projectFolder": "packages/node-engine/node", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/node-core-plugin", "projectFolder": "packages/plugins/node-core-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/node-variable-plugin", "projectFolder": "packages/plugins/node-variable-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/playground-react", "projectFolder": "packages/client/playground-react", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/redux-devtool-plugin", "projectFolder": "packages/plugins/redux-devtool-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/select-box-plugin", "projectFolder": "packages/plugins/select-box-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/shortcuts-plugin", "projectFolder": "packages/plugins/shortcuts-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/variable-core", "projectFolder": "packages/variable-engine/variable-core", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/variable-layout", "projectFolder": "packages/variable-engine/variable-layout", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/variable-plugin", "projectFolder": "packages/plugins/variable-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/command", "projectFolder": "packages/common/command", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/history", "projectFolder": "packages/common/history", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/history-storage", "projectFolder": "packages/common/history-storage", "versionPolicyName": "publishPolicy", - "tags": ["level-2", "team-flow"] + "tags": [ + "level-2", + "team-flow" + ] }, { "packageName": "@flowgram.ai/materials-plugin", "projectFolder": "packages/plugins/materials-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/fixed-semi-materials", "projectFolder": "packages/materials/fixed-semi-materials", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { - "packageName": "@flowgram.ai/form-materials", - "projectFolder": "packages/materials/form-materials", - "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "packageName": "@flowgram.ai/form-materials", + "projectFolder": "packages/materials/form-materials", + "versionPolicyName": "publishPolicy", + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/free-auto-layout-plugin", "projectFolder": "packages/plugins/free-auto-layout-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/i18n-plugin", "projectFolder": "packages/plugins/i18n-plugin", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/i18n", "projectFolder": "packages/common/i18n", "versionPolicyName": "publishPolicy", - "tags": ["level-1", "team-flow"] + "tags": [ + "level-1", + "team-flow" + ] }, { "packageName": "@flowgram.ai/demo-free-layout", "projectFolder": "apps/demo-free-layout", - "tags": ["level-1", "team-flow", "demo"], + "tags": [ + "level-1", + "team-flow", + "demo" + ], "versionPolicyName": "appPolicy" }, { "packageName": "@flowgram.ai/demo-fixed-layout-simple", "projectFolder": "apps/demo-fixed-layout-simple", "versionPolicyName": "appPolicy", - "tags": ["level-1", "team-flow", "demo"] + "tags": [ + "level-1", + "team-flow", + "demo" + ] }, { "packageName": "@flowgram.ai/demo-free-layout-simple", "projectFolder": "apps/demo-free-layout-simple", "versionPolicyName": "appPolicy", - "tags": ["level-1", "team-flow", "demo"] + "tags": [ + "level-1", + "team-flow", + "demo" + ] }, { "packageName": "@flowgram.ai/demo-node-form", "projectFolder": "apps/demo-node-form", - "tags": ["level-1", "team-flow", "demo"] + "tags": [ + "level-1", + "team-flow", + "demo" + ] }, { "packageName": "@flowgram.ai/demo-nextjs", "projectFolder": "apps/demo-nextjs", "versionPolicyName": "appPolicy", - "tags": ["level-1", "team-flow", "demo"] + "tags": [ + "level-1", + "team-flow", + "demo" + ] }, { "packageName": "@flowgram.ai/demo-react-16", "projectFolder": "apps/demo-react-16", "versionPolicyName": "appPolicy", - "tags": ["level-1", "team-flow", "demo"] + "tags": [ + "level-1", + "team-flow", + "demo" + ] }, { "packageName": "@flowgram.ai/demo-vite", "projectFolder": "apps/demo-vite", "versionPolicyName": "appPolicy", - "tags": ["level-1", "team-flow", "demo"] + "tags": [ + "level-1", + "team-flow", + "demo" + ] }, { "packageName": "@flowgram.ai/demo-playground", "projectFolder": "apps/demo-playground", "versionPolicyName": "appPolicy", - "tags": ["level-1", "team-flow", "demo"] + "tags": [ + "level-1", + "team-flow", + "demo" + ] + }, + { + "packageName": "@flowgram.ai/runtime-nodejs", + "projectFolder": "packages/runtime/nodejs", + "versionPolicyName": "appPolicy", + "tags": [ + "level-1", + "team-flow" + ] + }, + { + "packageName": "@flowgram.ai/runtime-js", + "projectFolder": "packages/runtime/js-core", + "versionPolicyName": "appPolicy", + "tags": [ + "level-1", + "team-flow" + ] + }, + { + "packageName": "@flowgram.ai/runtime-interface", + "projectFolder": "packages/runtime/interface", + "versionPolicyName": "appPolicy", + "tags": [ + "level-1", + "team-flow" + ] } ] }