-
diff --git a/Frontend/graphql/operations/archive-chat-conversation.graphql b/Frontend/graphql/operations/archive-chat-conversation.graphql
new file mode 100644
index 0000000..1b74f62
--- /dev/null
+++ b/Frontend/graphql/operations/archive-chat-conversation.graphql
@@ -0,0 +1,5 @@
+mutation ArchiveChatConversationMutation($id: ID!) {
+ archiveChatConversation(id: $id) {
+ ok
+ }
+}
diff --git a/Frontend/graphql/operations/chat-messages.graphql b/Frontend/graphql/operations/chat-messages.graphql
index cd73c6d..f439364 100644
--- a/Frontend/graphql/operations/chat-messages.graphql
+++ b/Frontend/graphql/operations/chat-messages.graphql
@@ -3,6 +3,10 @@ query ChatMessagesQuery {
id
role
text
+ requestId
+ eventType
+ phase
+ transient
thinking
tools
toolRuns {
diff --git a/Frontend/graphql/operations/dashboard.graphql b/Frontend/graphql/operations/dashboard.graphql
index e67ef1a..f3ca024 100644
--- a/Frontend/graphql/operations/dashboard.graphql
+++ b/Frontend/graphql/operations/dashboard.graphql
@@ -40,6 +40,16 @@ query DashboardQuery {
amount
nextStep
summary
+ currentStepId
+ steps {
+ id
+ title
+ description
+ status
+ dueAt
+ order
+ completedAt
+ }
}
feed {
id
diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json
index 32da408..4da4994 100644
--- a/Frontend/package-lock.json
+++ b/Frontend/package-lock.json
@@ -7,6 +7,7 @@
"name": "crm-frontend",
"hasInstallScript": true,
"dependencies": {
+ "@ai-sdk/vue": "^3.0.91",
"@langchain/core": "^0.3.77",
"@langchain/langgraph": "^0.2.74",
"@langchain/openai": "^0.6.9",
@@ -17,19 +18,88 @@
"@tiptap/extension-placeholder": "^2.27.2",
"@tiptap/starter-kit": "^2.27.2",
"@tiptap/vue-3": "^2.27.2",
+ "@xenova/transformers": "^2.17.2",
+ "ai": "^6.0.91",
"bullmq": "^5.58.2",
"daisyui": "^5.5.18",
"graphql": "^16.12.0",
"ioredis": "^5.7.0",
+ "langfuse": "^3.38.6",
+ "langsmith": "^0.5.4",
"nuxt": "^4.3.1",
"tailwindcss": "^4.1.18",
"vue": "^3.5.27",
+ "wavesurfer.js": "^7.12.1",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.29",
"zod": "^4.1.5"
},
"devDependencies": {
- "prisma": "^6.16.1"
+ "prisma": "^6.16.1",
+ "tsx": "^4.20.5"
+ }
+ },
+ "node_modules/@ai-sdk/gateway": {
+ "version": "3.0.50",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.50.tgz",
+ "integrity": "sha512-Jdd1a8VgbD7l7r+COj0h5SuaYRfPvOJ/AO6l0OrmTPEcI2MUQPr3C4JttfpNkcheEN+gOdy0CtZWuG17bW2fjw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "3.0.8",
+ "@ai-sdk/provider-utils": "4.0.15",
+ "@vercel/oidc": "3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
+ },
+ "node_modules/@ai-sdk/provider": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
+ "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "json-schema": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ai-sdk/provider-utils": {
+ "version": "4.0.15",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.15.tgz",
+ "integrity": "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "3.0.8",
+ "@standard-schema/spec": "^1.1.0",
+ "eventsource-parser": "^3.0.6"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
+ },
+ "node_modules/@ai-sdk/vue": {
+ "version": "3.0.91",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-3.0.91.tgz",
+ "integrity": "sha512-KyTLXAAFs2oe2tuvCLrZXPkAHyEhkA+FwQq661tDB7o0TyEPNNm99utcS/eN0mIps4WNC4+WphiiZwbcpGe80w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider-utils": "4.0.15",
+ "ai": "6.0.91",
+ "swrv": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.4"
}
},
"node_modules/@babel/code-frame": {
@@ -956,6 +1026,15 @@
"node": ">=18"
}
},
+ "node_modules/@huggingface/jinja": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz",
+ "integrity": "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@ioredis/commands": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz",
@@ -1088,6 +1167,40 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/@langchain/core/node_modules/langsmith": {
+ "version": "0.3.87",
+ "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.87.tgz",
+ "integrity": "sha512-XXR1+9INH8YX96FKWc5tie0QixWz6tOqAsAKfcJyPkE0xPep+NDz0IQLR32q4bn10QK3LqD2HN6T3n6z1YLW7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/uuid": "^10.0.0",
+ "chalk": "^4.1.2",
+ "console-table-printer": "^2.12.1",
+ "p-queue": "^6.6.2",
+ "semver": "^7.6.3",
+ "uuid": "^10.0.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "*",
+ "@opentelemetry/exporter-trace-otlp-proto": "*",
+ "@opentelemetry/sdk-trace-base": "*",
+ "openai": "*"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@opentelemetry/exporter-trace-otlp-proto": {
+ "optional": true
+ },
+ "@opentelemetry/sdk-trace-base": {
+ "optional": true
+ },
+ "openai": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@langchain/core/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
@@ -1710,6 +1823,15 @@
}
}
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/@oxc-minify/binding-android-arm-eabi": {
"version": "0.112.0",
"resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.112.0.tgz",
@@ -3268,6 +3390,70 @@
"@prisma/debug": "6.19.2"
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@remirror/core-constants": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
@@ -3813,7 +3999,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
@@ -4525,6 +4710,12 @@
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"license": "MIT"
},
+ "node_modules/@types/long": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
+ "license": "MIT"
+ },
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
@@ -4541,6 +4732,15 @@
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "25.3.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
+ "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -4607,6 +4807,15 @@
"node": ">=20"
}
},
+ "node_modules/@vercel/oidc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz",
+ "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz",
@@ -4922,6 +5131,20 @@
"integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==",
"license": "MIT"
},
+ "node_modules/@xenova/transformers": {
+ "version": "2.17.2",
+ "resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz",
+ "integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@huggingface/jinja": "^0.2.2",
+ "onnxruntime-web": "1.14.0",
+ "sharp": "^0.32.0"
+ },
+ "optionalDependencies": {
+ "onnxruntime-node": "1.14.0"
+ }
+ },
"node_modules/abbrev": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
@@ -4973,6 +5196,24 @@
"node": ">= 14"
}
},
+ "node_modules/ai": {
+ "version": "6.0.91",
+ "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.91.tgz",
+ "integrity": "sha512-k1/8BusZMhYVxxLZt0BUZzm9HVDCCh117nyWfWUx5xjR2+tWisJbXgysL7EBMq2lgyHwgpA1jDR3tVjWSdWZXw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/gateway": "3.0.50",
+ "@ai-sdk/provider": "3.0.8",
+ "@ai-sdk/provider-utils": "4.0.15",
+ "@opentelemetry/api": "1.9.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
+ },
"node_modules/alien-signals": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
@@ -5395,6 +5636,84 @@
}
}
},
+ "node_modules/bare-fs": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
+ "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-events": "^2.5.4",
+ "bare-path": "^3.0.0",
+ "bare-stream": "^2.6.4",
+ "bare-url": "^2.2.2",
+ "fast-fifo": "^1.3.2"
+ },
+ "engines": {
+ "bare": ">=1.16.0"
+ },
+ "peerDependencies": {
+ "bare-buffer": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
+ "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "bare": ">=1.14.0"
+ }
+ },
+ "node_modules/bare-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
+ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-os": "^3.0.1"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz",
+ "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "streamx": "^2.21.0",
+ "teex": "^1.0.1"
+ },
+ "peerDependencies": {
+ "bare-buffer": "*",
+ "bare-events": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ },
+ "bare-events": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-url": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
+ "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-path": "^3.0.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -5442,6 +5761,55 @@
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/bl/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -5820,6 +6188,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -5838,6 +6219,16 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
@@ -6294,6 +6685,30 @@
"node": ">=0.10.0"
}
},
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -6558,6 +6973,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
@@ -6726,6 +7150,15 @@
"bare-events": "^2.7.0"
}
},
+ "node_modules/eventsource-parser": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+ "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@@ -6749,6 +7182,15 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/exsolve": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
@@ -6853,6 +7295,12 @@
"node": ">=8"
}
},
+ "node_modules/flatbuffers": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz",
+ "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==",
+ "license": "SEE LICENSE IN LICENSE.txt"
+ },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -6891,6 +7339,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -6971,6 +7425,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.13.6",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
+ "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/giget": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/giget/-/giget-3.1.2.tgz",
@@ -6980,6 +7447,12 @@
"giget": "dist/cli.mjs"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
"node_modules/glob": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.3.tgz",
@@ -7059,6 +7532,12 @@
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
+ "node_modules/guid-typescript": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
+ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
+ "license": "ISC"
+ },
"node_modules/gzip-size": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz",
@@ -7305,6 +7784,12 @@
"url": "https://github.com/sponsors/brc-dd"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT"
+ },
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -7565,6 +8050,12 @@
"node": ">=6"
}
},
+ "node_modules/json-schema": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
+ "license": "(AFL-2.1 OR BSD-3-Clause)"
+ },
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -7601,10 +8092,34 @@
"integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==",
"license": "MIT"
},
+ "node_modules/langfuse": {
+ "version": "3.38.6",
+ "resolved": "https://registry.npmjs.org/langfuse/-/langfuse-3.38.6.tgz",
+ "integrity": "sha512-mtwfsNGIYvObRh+NYNGlJQJDiBN+Wr3Hnr++wN25mxuOpSTdXX+JQqVCyAqGL5GD2TAXRZ7COsN42Vmp9krYmg==",
+ "license": "MIT",
+ "dependencies": {
+ "langfuse-core": "^3.38.6"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/langfuse-core": {
+ "version": "3.38.6",
+ "resolved": "https://registry.npmjs.org/langfuse-core/-/langfuse-core-3.38.6.tgz",
+ "integrity": "sha512-EcZXa+DK9FJdi1I30+u19eKjuBJ04du6j2Nybk19KKCuraLczg/ppkTQcGvc4QOk//OAi3qUHrajUuV74RXsBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mustache": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/langsmith": {
- "version": "0.3.87",
- "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.87.tgz",
- "integrity": "sha512-XXR1+9INH8YX96FKWc5tie0QixWz6tOqAsAKfcJyPkE0xPep+NDz0IQLR32q4bn10QK3LqD2HN6T3n6z1YLW7Q==",
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.5.4.tgz",
+ "integrity": "sha512-qYkNIoKpf0ZYt+cYzrDV+XI3FCexApmZmp8EMs3eDTMv0OvrHMLoxJ9IpkeoXJSX24+GPk0/jXjKx2hWerpy9w==",
"license": "MIT",
"dependencies": {
"@types/uuid": "^10.0.0",
@@ -8061,6 +8576,12 @@
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
"license": "MIT"
},
+ "node_modules/long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+ "license": "Apache-2.0"
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -8286,6 +8807,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz",
@@ -8301,6 +8834,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -8328,6 +8870,12 @@
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
"node_modules/mlly": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
@@ -8448,6 +8996,12 @@
"integrity": "sha512-MUrzzDUcIOPbv7ubhDV/L4CIfVTATd9XhDE2ixFeCrM5yp9AlzUpn91JrnN0HD6hksdxvz9IW9aKANz0Bta0GA==",
"license": "MIT"
},
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
"node_modules/nitropack": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.13.1.tgz",
@@ -8557,6 +9111,18 @@
"url": "https://github.com/sponsors/sxzz"
}
},
+ "node_modules/node-abi": {
+ "version": "3.87.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
+ "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/node-abort-controller": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
@@ -8863,6 +9429,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
"node_modules/onetime": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
@@ -8878,6 +9453,50 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/onnx-proto": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz",
+ "integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==",
+ "license": "MIT",
+ "dependencies": {
+ "protobufjs": "^6.8.8"
+ }
+ },
+ "node_modules/onnxruntime-common": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz",
+ "integrity": "sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==",
+ "license": "MIT"
+ },
+ "node_modules/onnxruntime-node": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz",
+ "integrity": "sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==",
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32",
+ "darwin",
+ "linux"
+ ],
+ "dependencies": {
+ "onnxruntime-common": "~1.14.0"
+ }
+ },
+ "node_modules/onnxruntime-web": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz",
+ "integrity": "sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==",
+ "license": "MIT",
+ "dependencies": {
+ "flatbuffers": "^1.12.0",
+ "guid-typescript": "^1.0.9",
+ "long": "^4.0.0",
+ "onnx-proto": "^4.0.4",
+ "onnxruntime-common": "~1.14.0",
+ "platform": "^1.3.6"
+ }
+ },
"node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
@@ -9197,6 +9816,12 @@
"pathe": "^2.0.3"
}
},
+ "node_modules/platform": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
+ "license": "MIT"
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -9673,6 +10298,80 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/prebuild-install/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/pretty-bytes": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz",
@@ -9946,6 +10645,42 @@
"prosemirror-transform": "^1.1.0"
}
},
+ "node_modules/protobufjs": {
+ "version": "6.11.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
+ "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/long": "^4.0.1",
+ "@types/node": ">=13.7.0",
+ "long": "^4.0.0"
+ },
+ "bin": {
+ "pbjs": "bin/pbjs",
+ "pbts": "bin/pbts"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
@@ -10032,6 +10767,27 @@
"node": ">= 0.6"
}
},
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
"node_modules/rc9": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.0.tgz",
@@ -10175,6 +10931,16 @@
"node": ">=8"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "devOptional": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@@ -10452,6 +11218,35 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
+ "node_modules/sharp": {
+ "version": "0.32.6",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
+ "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.2",
+ "node-addon-api": "^6.1.0",
+ "prebuild-install": "^7.1.1",
+ "semver": "^7.5.4",
+ "simple-get": "^4.0.1",
+ "tar-fs": "^3.0.4",
+ "tunnel-agent": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/sharp/node_modules/node-addon-api": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
+ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -10497,6 +11292,51 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
"node_modules/simple-git": {
"version": "3.31.1",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.31.1.tgz",
@@ -10555,6 +11395,15 @@
"node": ">= 6"
}
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
"node_modules/simple-wcswidth": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
@@ -10767,6 +11616,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/strip-literal": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
@@ -10877,6 +11735,15 @@
"node": ">=16"
}
},
+ "node_modules/swrv": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/swrv/-/swrv-1.1.0.tgz",
+ "integrity": "sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "vue": ">=3.2.26 < 4"
+ }
+ },
"node_modules/system-architecture": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz",
@@ -10936,6 +11803,20 @@
"node": ">=18"
}
},
+ "node_modules/tar-fs": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
+ "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^4.0.1",
+ "bare-path": "^3.0.0"
+ }
+ },
"node_modules/tar-stream": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
@@ -10956,6 +11837,16 @@
"node": ">=18"
}
},
+ "node_modules/teex": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
+ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "streamx": "^2.12.5"
+ }
+ },
"node_modules/terser": {
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
@@ -11071,6 +11962,38 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/type-fest": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz",
@@ -11152,6 +12075,12 @@
"node": ">=18.12.0"
}
},
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "license": "MIT"
+ },
"node_modules/unenv": {
"version": "2.0.0-rc.24",
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
@@ -11995,6 +12924,12 @@
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
+ "node_modules/wavesurfer.js": {
+ "version": "7.12.1",
+ "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.12.1.tgz",
+ "integrity": "sha512-NswPjVHxk0Q1F/VMRemCPUzSojjuHHisQrBqQiRXg7MVbe3f5vQ6r0rTTXA/a/neC/4hnOEC4YpXca4LpH0SUg==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -12067,6 +13002,12 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
diff --git a/Frontend/package.json b/Frontend/package.json
index b573145..bac905e 100644
--- a/Frontend/package.json
+++ b/Frontend/package.json
@@ -13,9 +13,11 @@
"generate": "nuxt generate",
"postinstall": "nuxt prepare && prisma generate",
"preview": "nuxt preview",
- "typecheck": "nuxt typecheck"
+ "typecheck": "nuxt typecheck",
+ "worker:delivery": "tsx server/queues/worker.ts"
},
"dependencies": {
+ "@ai-sdk/vue": "^3.0.91",
"@langchain/core": "^0.3.77",
"@langchain/langgraph": "^0.2.74",
"@langchain/openai": "^0.6.9",
@@ -26,19 +28,25 @@
"@tiptap/extension-placeholder": "^2.27.2",
"@tiptap/starter-kit": "^2.27.2",
"@tiptap/vue-3": "^2.27.2",
+ "@xenova/transformers": "^2.17.2",
+ "ai": "^6.0.91",
"bullmq": "^5.58.2",
"daisyui": "^5.5.18",
"graphql": "^16.12.0",
"ioredis": "^5.7.0",
+ "langfuse": "^3.38.6",
+ "langsmith": "^0.5.4",
"nuxt": "^4.3.1",
"tailwindcss": "^4.1.18",
"vue": "^3.5.27",
+ "wavesurfer.js": "^7.12.1",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.29",
"zod": "^4.1.5"
},
"devDependencies": {
- "prisma": "^6.16.1"
+ "prisma": "^6.16.1",
+ "tsx": "^4.20.5"
},
"prisma": {
"seed": "node prisma/seed.mjs"
diff --git a/Frontend/prisma/schema.prisma b/Frontend/prisma/schema.prisma
index 6e79dc7..9f9be8d 100644
--- a/Frontend/prisma/schema.prisma
+++ b/Frontend/prisma/schema.prisma
@@ -3,7 +3,7 @@ generator client {
}
datasource db {
- provider = "sqlite"
+ provider = "postgresql"
url = env("DATABASE_URL")
}
@@ -263,22 +263,43 @@ model CalendarEvent {
}
model Deal {
- id String @id @default(cuid())
- teamId String
- contactId String
- title String
- stage String
- amount Int?
- nextStep String?
- summary String?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
+ id String @id @default(cuid())
+ teamId String
+ contactId String
+ title String
+ stage String
+ amount Int?
+ nextStep String?
+ summary String?
+ currentStepId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
- team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
- contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
+ team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
+ contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
+ steps DealStep[]
@@index([teamId, updatedAt])
@@index([contactId, updatedAt])
+ @@index([currentStepId])
+}
+
+model DealStep {
+ id String @id @default(cuid())
+ dealId String
+ title String
+ description String?
+ status String @default("todo")
+ dueAt DateTime?
+ order Int @default(0)
+ completedAt DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ deal Deal @relation(fields: [dealId], references: [id], onDelete: Cascade)
+
+ @@index([dealId, order])
+ @@index([status, dueAt])
}
model ChatConversation {
diff --git a/Frontend/prisma/seed.mjs b/Frontend/prisma/seed.mjs
index 9cdad85..1f13c10 100644
--- a/Frontend/prisma/seed.mjs
+++ b/Frontend/prisma/seed.mjs
@@ -32,7 +32,7 @@ const prisma = new PrismaClient();
const LOGIN_PHONE = "+15550000001";
const LOGIN_PASSWORD = "ConnectFlow#2026";
-const LOGIN_NAME = "Connect Owner";
+const LOGIN_NAME = "Владелец Connect";
const REF_DATE_ISO = "2026-02-20T12:00:00.000Z";
const SCRYPT_KEY_LENGTH = 64;
@@ -58,26 +58,26 @@ function plusMinutes(date, minutes) {
function buildOdooAiContacts(teamId) {
const prospects = [
- { name: "Olivia Reed", company: "RetailNova", country: "USA", location: "New York", email: "olivia.reed@retailnova.com", phone: "+1 555 120 0101" },
- { name: "Daniel Kim", company: "ForgePeak Manufacturing", country: "USA", location: "Chicago", email: "daniel.kim@forgepeak.com", phone: "+1 555 120 0102" },
- { name: "Marta Alonso", company: "Iberia Foods Group", country: "Spain", location: "Barcelona", email: "marta.alonso@iberiafoods.es", phone: "+34 91 555 0103" },
- { name: "Youssef Haddad", company: "GulfTrade Distribution", country: "UAE", location: "Dubai", email: "youssef.haddad@gulftrade.ae", phone: "+971 4 555 0104" },
- { name: "Emma Collins", company: "NorthBridge Logistics", country: "UK", location: "London", email: "emma.collins@northbridge.co.uk", phone: "+44 20 5550 0105" },
- { name: "Noah Fischer", company: "Bergmann Auto Parts", country: "Germany", location: "Munich", email: "noah.fischer@bergmann-auto.de", phone: "+49 89 5550 0106" },
- { name: "Ava Choi", company: "Pacific MedTech Supply", country: "Singapore", location: "Singapore", email: "ava.choi@pacificmedtech.sg", phone: "+65 6555 0107" },
- { name: "Liam Dubois", company: "HexaCommerce", country: "France", location: "Paris", email: "liam.dubois@hexacommerce.fr", phone: "+33 1 55 50 0108" },
- { name: "Maya Shah", company: "Zenith Consumer Brands", country: "Canada", location: "Toronto", email: "maya.shah@zenithbrands.ca", phone: "+1 416 555 0109" },
- { name: "Arman Petrosyan", company: "Ararat Electronics", country: "Armenia", location: "Yerevan", email: "arman.petrosyan@ararat-electronics.am", phone: "+374 10 555110" },
- { name: "Sophia Martinez", company: "Sunline Home Goods", country: "USA", location: "Austin", email: "sophia.martinez@sunlinehg.com", phone: "+1 555 120 0111" },
- { name: "Leo Novak", company: "CentralBuild Materials", country: "Germany", location: "Berlin", email: "leo.novak@centralbuild.de", phone: "+49 30 5550 0112" },
- { name: "Isla Grant", company: "BlueHarbor Pharma", country: "UK", location: "Manchester", email: "isla.grant@blueharbor.co.uk", phone: "+44 161 555 0113" },
- { name: "Mateo Rossi", company: "Milano Fashion House", country: "Italy", location: "Milan", email: "mateo.rossi@milanofh.it", phone: "+39 02 5550 0114" },
- { name: "Nina Volkova", company: "Polar AgriTech", country: "Kazakhstan", location: "Almaty", email: "nina.volkova@polaragri.kz", phone: "+7 727 555 0115" },
- { name: "Ethan Park", company: "Vertex Components", country: "South Korea", location: "Seoul", email: "ethan.park@vertexcomponents.kr", phone: "+82 2 555 0116" },
- { name: "Zara Khan", company: "Crescent Retail Chain", country: "UAE", location: "Abu Dhabi", email: "zara.khan@crescentretail.ae", phone: "+971 2 555 0117" },
- { name: "Hugo Silva", company: "Luso Industrial Systems", country: "Portugal", location: "Lisbon", email: "hugo.silva@lusois.pt", phone: "+351 21 555 0118" },
- { name: "Chloe Bernard", company: "Santex Clinics Network", country: "France", location: "Lyon", email: "chloe.bernard@santex.fr", phone: "+33 4 55 50 0119" },
- { name: "James Walker", company: "Metro Wholesale Group", country: "USA", location: "Los Angeles", email: "james.walker@metrowholesale.com", phone: "+1 555 120 0120" },
+ { name: "Оливия Рид", company: "РитейлНова", country: "США", location: "Нью-Йорк", email: "olivia.reed@retailnova.com", phone: "+1 555 120 0101" },
+ { name: "Даниэль Ким", company: "ФорджПик Производство", country: "США", location: "Чикаго", email: "daniel.kim@forgepeak.com", phone: "+1 555 120 0102" },
+ { name: "Марта Алонсо", company: "Иберия Фудс Групп", country: "Испания", location: "Барселона", email: "marta.alonso@iberiafoods.es", phone: "+34 91 555 0103" },
+ { name: "Юсеф Хаддад", company: "ГалфТрейд Дистрибуция", country: "ОАЭ", location: "Дубай", email: "youssef.haddad@gulftrade.ae", phone: "+971 4 555 0104" },
+ { name: "Эмма Коллинз", company: "НортБридж Логистика", country: "Великобритания", location: "Лондон", email: "emma.collins@northbridge.co.uk", phone: "+44 20 5550 0105" },
+ { name: "Ноа Фишер", company: "Бергман Автозапчасти", country: "Германия", location: "Мюнхен", email: "noah.fischer@bergmann-auto.de", phone: "+49 89 5550 0106" },
+ { name: "Ава Чой", company: "Пасифик МедТех Сапплай", country: "Сингапур", location: "Сингапур", email: "ava.choi@pacificmedtech.sg", phone: "+65 6555 0107" },
+ { name: "Лиам Дюбуа", company: "ГексаКоммерс", country: "Франция", location: "Париж", email: "liam.dubois@hexacommerce.fr", phone: "+33 1 55 50 0108" },
+ { name: "Майя Шах", company: "Зенит Консьюмер Брендс", country: "Канада", location: "Торонто", email: "maya.shah@zenithbrands.ca", phone: "+1 416 555 0109" },
+ { name: "Арман Петросян", company: "Арарат Электроникс", country: "Армения", location: "Ереван", email: "arman.petrosyan@ararat-electronics.am", phone: "+374 10 555110" },
+ { name: "София Мартинес", company: "Санлайн Товары для дома", country: "США", location: "Остин", email: "sophia.martinez@sunlinehg.com", phone: "+1 555 120 0111" },
+ { name: "Лео Новак", company: "ЦентралБилд Материалы", country: "Германия", location: "Берлин", email: "leo.novak@centralbuild.de", phone: "+49 30 5550 0112" },
+ { name: "Айла Грант", company: "БлюХарбор Фарма", country: "Великобритания", location: "Манчестер", email: "isla.grant@blueharbor.co.uk", phone: "+44 161 555 0113" },
+ { name: "Матео Росси", company: "Милано Фэшн Хаус", country: "Италия", location: "Милан", email: "mateo.rossi@milanofh.it", phone: "+39 02 5550 0114" },
+ { name: "Нина Волкова", company: "Полар АгриТех", country: "Казахстан", location: "Алматы", email: "nina.volkova@polaragri.kz", phone: "+7 727 555 0115" },
+ { name: "Итан Пак", company: "Вертекс Компонентс", country: "Южная Корея", location: "Сеул", email: "ethan.park@vertexcomponents.kr", phone: "+82 2 555 0116" },
+ { name: "Зара Хан", company: "Кресент Ритейл Чейн", country: "ОАЭ", location: "Абу-Даби", email: "zara.khan@crescentretail.ae", phone: "+971 2 555 0117" },
+ { name: "Уго Силва", company: "Лузо Индастриал Системс", country: "Португалия", location: "Лиссабон", email: "hugo.silva@lusois.pt", phone: "+351 21 555 0118" },
+ { name: "Хлоя Бернар", company: "Сантекс Сеть Клиник", country: "Франция", location: "Лион", email: "chloe.bernard@santex.fr", phone: "+33 4 55 50 0119" },
+ { name: "Джеймс Уокер", company: "Метро Оптовая Группа", country: "США", location: "Лос-Анджелес", email: "james.walker@metrowholesale.com", phone: "+1 555 120 0120" },
];
return prospects.map((p, idx) => {
@@ -113,8 +113,8 @@ async function main() {
const team = await prisma.team.upsert({
where: { id: "demo-team" },
- update: { name: "Connect Workspace" },
- create: { id: "demo-team", name: "Connect Workspace" },
+ update: { name: "Connect Рабочее пространство" },
+ create: { id: "demo-team", name: "Connect Рабочее пространство" },
});
await prisma.teamMember.upsert({
@@ -125,8 +125,8 @@ async function main() {
const conversation = await prisma.chatConversation.upsert({
where: { id: `pilot-${team.id}` },
- update: { title: "Pilot" },
- create: { id: `pilot-${team.id}`, teamId: team.id, createdByUserId: user.id, title: "Pilot" },
+ update: { title: "Пилот" },
+ create: { id: `pilot-${team.id}`, teamId: team.id, createdByUserId: user.id, title: "Пилот" },
});
await prisma.$transaction([
@@ -150,22 +150,22 @@ async function main() {
});
const integrationModules = [
- "Sales + CRM + forecasting copilot",
- "Inventory + demand prediction",
- "Purchase + supplier risk scoring",
- "Accounting + AI anomaly detection",
- "Helpdesk + ticket triage assistant",
- "Manufacturing + production planning AI",
+ "Продажи + CRM + копилот прогнозирования",
+ "Склад + прогноз спроса",
+ "Закупки + оценка рисков поставщиков",
+ "Бухгалтерия + AI-детекция аномалий",
+ "Поддержка + ассистент триажа заявок",
+ "Производство + AI-планирование мощностей",
];
await prisma.contactNote.createMany({
data: contacts.map((c, idx) => ({
contactId: c.id,
content:
- `${c.company ?? c.name} is evaluating Odoo implementation with AI extensions. ` +
- `Primary integration scope: ${integrationModules[idx % integrationModules.length]}. ` +
- `Main buying trigger: reduce manual operations and shorten decision cycles. ` +
- `Next milestone: run discovery workshop, confirm data owners, and approve pilot KPI pack.`,
+ `${c.company ?? c.name} рассматривает внедрение Odoo с AI-расширениями. ` +
+ `Основной контур интеграции: ${integrationModules[idx % integrationModules.length]}. ` +
+ `Ключевой драйвер покупки: сократить ручные операции и ускорить цикл принятия решений. ` +
+ `Следующая веха: провести сессию уточнения, согласовать владельцев данных и утвердить KPI пилота.`,
})),
});
@@ -180,7 +180,7 @@ async function main() {
kind: "MESSAGE",
direction: "IN",
channel: channels[i % channels.length],
- content: `Hi, we are reviewing Odoo + AI rollout for ${contact.company}. Can we align on integration timeline this week?`,
+ content: `Здравствуйте! Мы рассматриваем запуск Odoo + AI для ${contact.company}. Можем согласовать план интеграции на этой неделе?`,
occurredAt: base,
});
@@ -189,7 +189,7 @@ async function main() {
kind: "MESSAGE",
direction: "OUT",
channel: channels[(i + 1) % channels.length],
- content: "Sure. I suggest a 45-min discovery focused on workflows, API constraints, and pilot KPIs.",
+ content: "Да, предлагаю 45-минутный разбор: процессы, ограничения API и KPI пилота.",
occurredAt: plusMinutes(base, 22),
});
@@ -198,7 +198,7 @@ async function main() {
kind: "MESSAGE",
direction: i % 3 === 0 ? "OUT" : "IN",
channel: channels[(i + 2) % channels.length],
- content: "Status update: technical scope is clear; blocker is budget owner approval and security questionnaire.",
+ content: "Обновление статуса: технический объём ясен; блокер — согласование бюджета и анкета по безопасности.",
occurredAt: plusMinutes(base, 65),
});
@@ -208,11 +208,11 @@ async function main() {
kind: "CALL",
direction: "OUT",
channel: "PHONE",
- content: "Discovery call: Odoo modules, data flows, AI use-cases",
+ content: "Созвон по уточнению: модули Odoo, потоки данных и AI-сценарии",
durationSec: 180 + ((i * 23) % 420),
transcriptJson: [
- `${contact.name}: We need phased rollout, starting from Sales and Inventory.`,
- "You: Agreed. We can run a 6-week pilot with KPI baseline and weekly checkpoints.",
+ `${contact.name}: Нам нужен поэтапный запуск, начнём с продаж и склада.`,
+ "Вы: Согласен. Делаем пилот на 6 недель с базовыми KPI и еженедельными контрольными точками.",
],
occurredAt: plusMinutes(base, 110),
});
@@ -222,47 +222,47 @@ async function main() {
await prisma.calendarEvent.createMany({
data: contacts.flatMap((c, idx) => {
- // Historical week ending on 20 Feb 2026: all seeded meetings are completed.
+ // Историческая неделя до 20 Feb 2026: все сидовые встречи завершены.
const firstStart = atOffset(-6 + (idx % 5), 10 + (idx % 6), (idx * 5) % 60);
const secondStart = atOffset(-5 + (idx % 5), 14 + (idx % 4), (idx * 3) % 60);
return [
{
teamId: team.id,
contactId: c.id,
- title: `Discovery: Odoo + AI with ${c.company ?? c.name}`,
+ title: `Сессия уточнения: Odoo + AI с ${c.company ?? c.name}`,
startsAt: firstStart,
endsAt: plusMinutes(firstStart, 30),
- note: "Confirm integration scope, current stack, and pilot success metrics.",
+ note: "Подтвердить рамки интеграции, текущий стек и метрики успеха пилота.",
status: "done",
},
{
teamId: team.id,
contactId: c.id,
- title: `Architecture workshop: ${c.company ?? c.name}`,
+ title: `Архитектурный воркшоп: ${c.company ?? c.name}`,
startsAt: secondStart,
endsAt: plusMinutes(secondStart, 45),
- note: "Review API mapping, ETL boundaries, and AI assistant guardrails.",
+ note: "Проверить маппинг API, границы ETL и ограничения для AI-ассистента.",
status: "done",
},
];
}),
});
- const stages = ["Lead", "Discovery", "Solution Fit", "Proposal", "Negotiation", "Pilot", "Contract Review"];
+ const stages = ["Лид", "Уточнение", "Подбор решения", "Коммерческое предложение", "Переговоры", "Пилот", "Проверка договора"];
await prisma.deal.createMany({
data: contacts.map((c, idx) => ({
teamId: team.id,
contactId: c.id,
- title: `${c.company ?? "Account"} Odoo + AI integration`,
+ title: `${c.company ?? "Клиент"}: интеграция Odoo + AI`,
stage: stages[idx % stages.length],
amount: 18000 + (idx % 8) * 7000,
nextStep:
idx % 4 === 0
- ? "Send pilot proposal and finalize integration backlog."
- : "Run solution workshop and align commercial owner on timeline.",
+ ? "Отправить предложение по пилоту и зафиксировать список задач интеграции."
+ : "Провести воркшоп по решению и согласовать сроки с коммерческим владельцем.",
summary:
- "Potential deal for phased Odoo implementation with AI copilots for ops, sales, and planning. " +
- "Commercial model: discovery + pilot + rollout.",
+ "Потенциальная сделка на поэтапное внедрение Odoo с AI-копилотами для операций, продаж и планирования. " +
+ "Коммерческая модель: уточнение + пилот + тиражирование.",
})),
});
@@ -272,8 +272,8 @@ async function main() {
contactId: c.id,
text:
idx % 3 === 0
- ? "Pinned: ask for ERP owner, data owner, and target go-live quarter."
- : "Pinned: keep communication around one KPI and one next action.",
+ ? "Закреплено: уточнить владельца ERP, владельца данных и целевой квартал запуска."
+ : "Закреплено: держать коммуникацию вокруг одного KPI и следующего шага.",
})),
});
@@ -287,14 +287,14 @@ async function main() {
contactId: c.id,
happenedAt: atOffset(-(idx % 6), 9 + (idx % 8), (idx * 9) % 60),
text:
- `I reviewed ${c.company ?? c.name} account activity for the Odoo + AI opportunity. ` +
- "There is enough momentum to move the deal one stage with a concrete next action.",
+ `Я проверил активность по аккаунту ${c.company ?? c.name} в рамках сделки Odoo + AI. ` +
+ "Есть достаточный импульс, чтобы перевести сделку на следующий этап при чётком следующем шаге.",
proposalJson: {
- title: idx % 2 === 0 ? "Schedule pilot scoping call" : "Send unblock note for budget owner",
+ title: idx % 2 === 0 ? "Назначить созвон по рамкам пилота" : "Отправить сообщение для разблокировки у владельца бюджета",
details: [
- `Contact: ${c.name}`,
- idx % 2 === 0 ? "Timing: this week, 45 minutes" : "Timing: today in primary channel",
- "Goal: confirm scope, owner, and next commercial checkpoint",
+ `Контакт: ${c.name}`,
+ idx % 2 === 0 ? "Когда: на этой неделе, 45 минут" : "Когда: сегодня в основном канале",
+ "Цель: подтвердить объём, владельца и следующую коммерческую контрольную точку",
],
key: proposalKeys[idx % proposalKeys.length],
},
@@ -305,62 +305,62 @@ async function main() {
data: [
{
teamId: team.id,
- title: "Odoo integration discovery checklist",
+ title: "Чеклист уточнения для интеграции Odoo",
type: "Regulation",
- owner: "Solution Team",
- scope: "Pre-sale discovery",
- summary: "Mandatory questions before estimation of Odoo + AI rollout.",
- body: "## Must capture\n- Current ERP modules\n- Integration endpoints\n- Data owner per domain\n- Security constraints\n- Pilot KPI baseline",
+ owner: "Команда решений",
+ scope: "Предпродажное уточнение",
+ summary: "Обязательные вопросы перед оценкой запуска Odoo + AI.",
+ body: "## Нужно зафиксировать\n- Текущие модули ERP\n- Точки интеграции\n- Владельца данных по каждому домену\n- Ограничения безопасности\n- Базовые KPI пилота",
updatedAt: atOffset(-1, 11, 10),
},
{
teamId: team.id,
- title: "AI copilot playbook for Odoo",
+ title: "Плейбук AI-копилота для Odoo",
type: "Playbook",
- owner: "AI Practice Lead",
- scope: "Use-case qualification",
- summary: "How to position forecasting, assistant, and anomaly detection features.",
- body: "## Flow\n1. Process pain\n2. Data quality\n3. Model target\n4. Success KPI\n5. Pilot scope",
+ owner: "Лид AI-практики",
+ scope: "Квалификация сценариев",
+ summary: "Как позиционировать прогнозирование, ассистента и детекцию аномалий.",
+ body: "## Поток\n1. Боль процесса\n2. Качество данных\n3. Целевая модель\n4. KPI успеха\n5. Объём пилота",
updatedAt: atOffset(-2, 15, 0),
},
{
teamId: team.id,
- title: "Pilot pricing matrix",
+ title: "Матрица цен для пилота",
type: "Policy",
- owner: "Commercial Ops",
- scope: "Discovery and pilot contracts",
- summary: "Price ranges for discovery, pilot, and production rollout phases.",
- body: "## Typical ranges\n- Discovery: 5k-12k\n- Pilot: 15k-45k\n- Rollout: 50k+\n\nAlways tie cost to scope and timeline.",
+ owner: "Коммерческие операции",
+ scope: "Контракты уточнения и пилота",
+ summary: "Диапазоны цен для уточнения, пилота и продуктивной фазы.",
+ body: "## Типовые диапазоны\n- Уточнение: 5k-12k\n- Пилот: 15k-45k\n- Тиражирование: 50k+\n\nВсегда привязывай стоимость к объёму и срокам.",
updatedAt: atOffset(-3, 9, 30),
},
{
teamId: team.id,
- title: "Security and compliance template",
+ title: "Шаблон по безопасности и комплаенсу",
type: "Template",
- owner: "Delivery Office",
- scope: "Enterprise prospects",
- summary: "Template answers for data residency, RBAC, audit trail, and PII handling.",
- body: "## Sections\n- Hosting model\n- Access control\n- Logging and audit\n- Data retention\n- Incident response",
+ owner: "Офис внедрения",
+ scope: "Крупные клиенты",
+ summary: "Шаблон ответов по data residency, RBAC, аудиту и обработке PII.",
+ body: "## Разделы\n- Модель хостинга\n- Контроль доступа\n- Логирование и аудит\n- Срок хранения данных\n- Реакция на инциденты",
updatedAt: atOffset(-4, 13, 45),
},
{
teamId: team.id,
- title: "Integration architecture blueprint",
+ title: "Референс интеграционной архитектуры",
type: "Playbook",
- owner: "Architecture Team",
- scope: "Technical workshops",
- summary: "Reference architecture for Odoo connectors, ETL, and AI service layer.",
- body: "## Layers\n- Odoo core modules\n- Integration bus\n- Data warehouse\n- AI service endpoints\n- Monitoring",
+ owner: "Архитектурная команда",
+ scope: "Технические воркшопы",
+ summary: "Референс-архитектура для коннекторов Odoo, ETL и AI-сервисного слоя.",
+ body: "## Слои\n- Базовые модули Odoo\n- Интеграционная шина\n- Хранилище данных\n- Эндпоинты AI-сервиса\n- Мониторинг",
updatedAt: atOffset(-5, 10, 0),
},
{
teamId: team.id,
- title: "Go-live readiness checklist",
+ title: "Чеклист готовности к запуску",
type: "Regulation",
owner: "PMO",
- scope: "Pilot to production transition",
- summary: "Checklist to move from pilot acceptance to production launch.",
- body: "## Required\n- Pilot KPIs approved\n- Rollout backlog prioritized\n- Owners assigned\n- Support model defined",
+ scope: "Переход от пилота к продакшену",
+ summary: "Чеклист перехода от приёмки пилота к запуску в прод.",
+ body: "## Обязательно\n- KPI пилота утверждены\n- Backlog тиражирования приоритизирован\n- Владельцы назначены\n- Модель поддержки определена",
updatedAt: atOffset(-6, 16, 15),
},
],
diff --git a/Frontend/public/audio-samples/meeting-recording-2026-02-11.webm b/Frontend/public/audio-samples/meeting-recording-2026-02-11.webm
new file mode 100644
index 0000000..c0f14dd
Binary files /dev/null and b/Frontend/public/audio-samples/meeting-recording-2026-02-11.webm differ
diff --git a/Frontend/public/audio-samples/national-road-9.m4a b/Frontend/public/audio-samples/national-road-9.m4a
new file mode 100644
index 0000000..78440a8
Binary files /dev/null and b/Frontend/public/audio-samples/national-road-9.m4a differ
diff --git a/Frontend/scripts/compose-dev.sh b/Frontend/scripts/compose-dev.sh
index 73d2d77..313322f 100755
--- a/Frontend/scripts/compose-dev.sh
+++ b/Frontend/scripts/compose-dev.sh
@@ -11,22 +11,32 @@ find .output -mindepth 1 -maxdepth 1 -exec rm -rf {} + || true
rm -rf node_modules/.cache node_modules/.vite
# Install deps (container starts from a clean image).
-# Fallback to npm install when lockfile was produced by a newer npm major.
-if ! npm ci; then
- npm install
-fi
+# npm ci is unstable in this workspace due lock drift in transitive deps.
+npm install
-# DB path used by DATABASE_URL="file:../../.data/clientsflow-dev.db" from /app/Frontend
-DB_FILE="/app/.data/clientsflow-dev.db"
-
-# First boot: create schema + seed.
-# Next boots: keep data, only sync schema and re-run idempotent seed.
-if [[ ! -f "$DB_FILE" ]]; then
- npx prisma db push --force-reset
+# sharp is a native module and can break when cached node_modules were installed
+# for a different CPU variant (for example arm64v8). Force a local rebuild.
+ARCH="$(uname -m)"
+if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
+ npm rebuild sharp --platform=linux --arch=arm64v8 \
+ || npm rebuild sharp --platform=linux --arch=arm64 \
+ || npm install sharp --platform=linux --arch=arm64v8 --save-exact=false \
+ || npm install sharp --platform=linux --arch=arm64 --save-exact=false
+elif [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "amd64" ]; then
+ npm rebuild sharp --platform=linux --arch=x64 \
+ || npm install sharp --platform=linux --arch=x64 --save-exact=false
else
- npx prisma db push
+ npm rebuild sharp || true
fi
+# Wait until PostgreSQL is reachable before applying schema.
+until node -e "const u=new URL(process.env.DATABASE_URL||''); const net=require('net'); const s=net.createConnection({host:u.hostname,port:Number(u.port||5432)}); s.on('connect',()=>{s.end(); process.exit(0);}); s.on('error',()=>process.exit(1)); setTimeout(()=>process.exit(1), 1000);" ; do
+ echo "Waiting for PostgreSQL..."
+ sleep 1
+done
+
+npx prisma db push
+
node prisma/seed.mjs
exec npm run dev -- --host 0.0.0.0 --port 3000
diff --git a/Frontend/scripts/compose-worker.sh b/Frontend/scripts/compose-worker.sh
new file mode 100644
index 0000000..43180c3
--- /dev/null
+++ b/Frontend/scripts/compose-worker.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+# Worker container starts from clean image.
+# Install deps without frontend postinstall hooks (nuxt prepare) to keep worker lean/stable.
+npm install --ignore-scripts
+ARCH="$(uname -m)"
+if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
+ npm rebuild sharp --platform=linux --arch=arm64v8 \
+ || npm rebuild sharp --platform=linux --arch=arm64 \
+ || npm install sharp --platform=linux --arch=arm64v8 --save-exact=false \
+ || npm install sharp --platform=linux --arch=arm64 --save-exact=false
+elif [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "amd64" ]; then
+ npm rebuild sharp --platform=linux --arch=x64 \
+ || npm install sharp --platform=linux --arch=x64 --save-exact=false
+else
+ npm rebuild sharp || true
+fi
+npx prisma generate
+
+# Ensure DB is reachable before the worker starts consuming jobs.
+until node -e "const u=new URL(process.env.DATABASE_URL||''); const net=require('net'); const s=net.createConnection({host:u.hostname,port:Number(u.port||5432)}); s.on('connect',()=>{s.end(); process.exit(0);}); s.on('error',()=>process.exit(1)); setTimeout(()=>process.exit(1), 1000);" ; do
+ echo "Waiting for PostgreSQL..."
+ sleep 1
+done
+
+exec npm run worker:delivery
diff --git a/Frontend/server/agent/crmAgent.ts b/Frontend/server/agent/crmAgent.ts
index 212c8ae..6254ffc 100644
--- a/Frontend/server/agent/crmAgent.ts
+++ b/Frontend/server/agent/crmAgent.ts
@@ -97,6 +97,8 @@ export async function runCrmAgentFor(
teamId: string;
userId: string;
userText: string;
+ requestId?: string;
+ conversationId?: string;
onTrace?: (event: AgentTraceEvent) => Promise
| void;
},
): Promise {
@@ -246,29 +248,23 @@ export async function persistChatMessage(input: {
at: string;
}>;
changeSet?: ChangeSet | null;
+ requestId?: string;
+ eventType?: "user" | "trace" | "assistant" | "note";
+ phase?: "pending" | "running" | "final" | "error";
+ transient?: boolean;
teamId: string;
conversationId: string;
authorUserId?: string | null;
}) {
- const hasDebugPayload = Boolean(
- (input.plan && input.plan.length) ||
- (input.tools && input.tools.length) ||
- (input.thinking && input.thinking.length) ||
- (input.toolRuns && input.toolRuns.length) ||
- input.changeSet,
- );
+ const hasStoredPayload = Boolean(input.changeSet);
const data: Prisma.ChatMessageCreateInput = {
team: { connect: { id: input.teamId } },
conversation: { connect: { id: input.conversationId } },
authorUser: input.authorUserId ? { connect: { id: input.authorUserId } } : undefined,
role: input.role,
text: input.text,
- planJson: hasDebugPayload
+ planJson: hasStoredPayload
? ({
- steps: input.plan ?? [],
- tools: input.tools ?? [],
- thinking: input.thinking ?? input.plan ?? [],
- toolRuns: input.toolRuns ?? [],
changeSet: input.changeSet ?? null,
} as any)
: undefined,
diff --git a/Frontend/server/agent/langgraphCrmAgent.ts b/Frontend/server/agent/langgraphCrmAgent.ts
index d16c85b..97a59b7 100644
--- a/Frontend/server/agent/langgraphCrmAgent.ts
+++ b/Frontend/server/agent/langgraphCrmAgent.ts
@@ -6,11 +6,23 @@ import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
+import { getLangfuseClient } from "../utils/langfuse";
function iso(d: Date) {
return d.toISOString();
}
+function cyclePrompt(userText: string, cycle: number, cycleNotes: string[], pendingCount: number) {
+ if (cycle === 1) return userText;
+ return [
+ "Continue solving the same user request.",
+ `User request: ${userText}`,
+ cycleNotes.length ? `Progress notes:\n- ${cycleNotes.join("\n- ")}` : "No progress notes yet.",
+ `Pending staged changes: ${pendingCount}.`,
+ "Do the next useful step. If done, produce final concise answer.",
+ ].join("\n");
+}
+
type GigachatTokenCache = {
token: string;
expiresAtSec: number;
@@ -322,6 +334,8 @@ export async function runLangGraphCrmAgentFor(input: {
teamId: string;
userId: string;
userText: string;
+ requestId?: string;
+ conversationId?: string;
onTrace?: (event: AgentTraceEvent) => Promise | void;
}): Promise {
const openrouterApiKey = (process.env.OPENROUTER_API_KEY ?? "").trim();
@@ -414,6 +428,16 @@ export async function runLangGraphCrmAgentFor(input: {
const pendingChanges: PendingChange[] = [];
async function emitTrace(event: AgentTraceEvent) {
+ lfTrace?.event({
+ name: "agent.trace",
+ input: {
+ text: event.text,
+ toolRun: event.toolRun ?? null,
+ },
+ metadata: {
+ requestId: input.requestId ?? null,
+ },
+ });
if (!input.onTrace) return;
try {
await input.onTrace(event);
@@ -544,7 +568,7 @@ export async function runLangGraphCrmAgentFor(input: {
const toolName = `crm:${raw.action}`;
const startedAt = new Date().toISOString();
toolsUsed.push(toolName);
- await emitTrace({ text: `Tool started: ${toolName}` });
+ await emitTrace({ text: `Использую инструмент: ${toolName}` });
const executeAction = async () => {
if (raw.action === "get_snapshot") {
@@ -856,6 +880,23 @@ export async function runLangGraphCrmAgentFor(input: {
const maxCycles = Math.max(1, Math.min(Number(process.env.CF_AGENT_MAX_CYCLES ?? "3"), 8));
const cycleTimeoutMs = Math.max(5000, Math.min(Number(process.env.CF_AGENT_CYCLE_TIMEOUT_MS ?? "1200000"), 1800000));
+ const tracingFlag = (process.env.LANGSMITH_TRACING ?? process.env.LANGCHAIN_TRACING_V2 ?? "").trim().toLowerCase();
+ const tracingEnabled = tracingFlag === "1" || tracingFlag === "true" || tracingFlag === "yes";
+ const langfuse = getLangfuseClient();
+ const lfTrace = langfuse?.trace({
+ id: input.requestId ?? makeId("trace"),
+ name: "clientsflow.crm_agent_request",
+ userId: input.userId,
+ sessionId: input.conversationId ?? undefined,
+ input: input.userText,
+ metadata: {
+ teamId: input.teamId,
+ userId: input.userId,
+ requestId: input.requestId ?? null,
+ conversationId: input.conversationId ?? null,
+ },
+ tags: ["clientsflow", "crm-agent", "langgraph"],
+ });
let consecutiveNoProgress = 0;
let finalText = "";
const cycleNotes: string[] = [];
@@ -931,24 +972,34 @@ export async function runLangGraphCrmAgentFor(input: {
};
for (let cycle = 1; cycle <= maxCycles; cycle += 1) {
- await emitTrace({ text: `Cycle ${cycle}/${maxCycles}: start` });
+ const userPrompt = cyclePrompt(input.userText, cycle, cycleNotes, pendingChanges.length);
+ const cycleSpan = lfTrace?.span({
+ name: "agent.cycle",
+ input: userPrompt,
+ metadata: {
+ cycle,
+ requestId: input.requestId ?? null,
+ },
+ });
+ await emitTrace({ text: "Анализирую задачу и текущий контекст CRM." });
const beforeRuns = toolRuns.length;
const beforeWrites = dbWrites.length;
const beforePending = pendingChanges.length;
- const userPrompt =
- cycle === 1
- ? input.userText
- : [
- "Continue solving the same user request.",
- `User request: ${input.userText}`,
- cycleNotes.length ? `Progress notes:\n- ${cycleNotes.join("\n- ")}` : "No progress notes yet.",
- `Pending staged changes: ${pendingChanges.length}.`,
- "Do the next useful step. If done, produce final concise answer.",
- ].join("\n");
-
let res: any;
try {
+ const invokeConfig: Record = { recursionLimit: 30 };
+ if (tracingEnabled) {
+ invokeConfig.runName = "clientsflow.crm_agent_cycle";
+ invokeConfig.tags = ["clientsflow", "crm-agent", "langgraph"];
+ invokeConfig.metadata = {
+ teamId: input.teamId,
+ userId: input.userId,
+ requestId: input.requestId ?? null,
+ conversationId: input.conversationId ?? null,
+ cycle,
+ };
+ }
res = await Promise.race([
agent.invoke(
{
@@ -957,14 +1008,19 @@ export async function runLangGraphCrmAgentFor(input: {
{ role: "user", content: userPrompt },
],
},
- { recursionLimit: 30 },
+ invokeConfig,
),
new Promise((_resolve, reject) =>
setTimeout(() => reject(new Error(`Cycle timeout after ${cycleTimeoutMs}ms`)), cycleTimeoutMs),
),
]);
} catch (e: any) {
- await emitTrace({ text: `Cycle ${cycle}/${maxCycles}: failed (${String(e?.message || e)})` });
+ await emitTrace({ text: "Один из шагов завершился ошибкой, пробую безопасный обход." });
+ cycleSpan?.end({
+ output: "error",
+ level: "ERROR",
+ statusMessage: String(e?.message ?? e ?? "unknown_error"),
+ });
if (!finalText) {
finalText = "Не удалось завершить задачу за отведенное время. Уточни запрос или сократи объем.";
}
@@ -978,12 +1034,23 @@ export async function runLangGraphCrmAgentFor(input: {
const progressed =
toolRuns.length > beforeRuns || dbWrites.length > beforeWrites || pendingChanges.length !== beforePending;
+ cycleSpan?.end({
+ output: parsed.text || "",
+ metadata: {
+ progressed,
+ toolRunsDelta: toolRuns.length - beforeRuns,
+ dbWritesDelta: dbWrites.length - beforeWrites,
+ pendingDelta: pendingChanges.length - beforePending,
+ },
+ });
if (progressed) {
cycleNotes.push(`Cycle ${cycle}: updated tools/data state.`);
}
await emitTrace({
- text: `Cycle ${cycle}/${maxCycles}: ${progressed ? "progress" : "no progress"} · pending=${pendingChanges.length}`,
+ text: progressed
+ ? "Продвигаюсь по задаче и обновляю рабочий набор изменений."
+ : "Промежуточный шаг не дал прогресса, проверяю следующий вариант.",
});
if (!progressed) {
@@ -994,16 +1061,28 @@ export async function runLangGraphCrmAgentFor(input: {
const done = (!progressed && cycle > 1) || cycle === maxCycles;
if (done) {
- await emitTrace({ text: `Cycle ${cycle}/${maxCycles}: done` });
+ await emitTrace({ text: "Формирую итоговый ответ." });
break;
}
if (consecutiveNoProgress >= 2) {
- await emitTrace({ text: `Cycle ${cycle}/${maxCycles}: stopped (no progress)` });
+ await emitTrace({ text: "Останавливаюсь, чтобы не крутиться в пустом цикле." });
break;
}
}
+ lfTrace?.update({
+ output: finalText || null,
+ metadata: {
+ toolsUsedCount: toolsUsed.length,
+ toolRunsCount: toolRuns.length,
+ dbWritesCount: dbWrites.length,
+ pendingChangesCount: pendingChanges.length,
+ maxCycles,
+ },
+ });
+ void langfuse?.flushAsync().catch(() => {});
+
if (!finalText) {
throw new Error("Model returned empty response");
}
diff --git a/Frontend/server/api/omni/delivery/enqueue.post.ts b/Frontend/server/api/omni/delivery/enqueue.post.ts
new file mode 100644
index 0000000..50096d8
--- /dev/null
+++ b/Frontend/server/api/omni/delivery/enqueue.post.ts
@@ -0,0 +1,62 @@
+import { readBody } from "h3";
+import { getAuthContext } from "../../../utils/auth";
+import { prisma } from "../../../utils/prisma";
+import { enqueueOutboundDelivery } from "../../../queues/outboundDelivery";
+
+type EnqueueBody = {
+ omniMessageId?: string;
+ endpoint?: string;
+ method?: "POST" | "PUT" | "PATCH";
+ headers?: Record;
+ payload?: unknown;
+ timeoutMs?: number;
+ provider?: string;
+ channel?: string;
+ attempts?: number;
+};
+
+export default defineEventHandler(async (event) => {
+ const auth = await getAuthContext(event);
+ const body = await readBody(event);
+
+ const omniMessageId = String(body?.omniMessageId ?? "").trim();
+ const endpoint = String(body?.endpoint ?? "").trim();
+ if (!omniMessageId) {
+ throw createError({ statusCode: 400, statusMessage: "omniMessageId is required" });
+ }
+ if (!endpoint) {
+ throw createError({ statusCode: 400, statusMessage: "endpoint is required" });
+ }
+
+ const msg = await prisma.omniMessage.findFirst({
+ where: { id: omniMessageId, teamId: auth.teamId },
+ select: { id: true },
+ });
+ if (!msg) {
+ throw createError({ statusCode: 404, statusMessage: "omni message not found" });
+ }
+
+ const attempts = Math.max(1, Math.min(Number(body?.attempts ?? 12), 50));
+ const job = await enqueueOutboundDelivery(
+ {
+ omniMessageId,
+ endpoint,
+ method: body?.method ?? "POST",
+ headers: body?.headers ?? {},
+ payload: body?.payload ?? {},
+ timeoutMs: body?.timeoutMs,
+ provider: body?.provider ?? undefined,
+ channel: body?.channel ?? undefined,
+ },
+ {
+ attempts,
+ },
+ );
+
+ return {
+ ok: true,
+ queue: "omni-outbound",
+ jobId: job.id,
+ omniMessageId,
+ };
+});
diff --git a/Frontend/server/api/omni/telegram/send.post.ts b/Frontend/server/api/omni/telegram/send.post.ts
new file mode 100644
index 0000000..141d0a5
--- /dev/null
+++ b/Frontend/server/api/omni/telegram/send.post.ts
@@ -0,0 +1,32 @@
+import { readBody } from "h3";
+import { getAuthContext } from "../../../utils/auth";
+import { prisma } from "../../../utils/prisma";
+import { enqueueTelegramSend } from "../../../queues/telegramSend";
+
+export default defineEventHandler(async (event) => {
+ const auth = await getAuthContext(event);
+ const body = await readBody<{ omniMessageId?: string; attempts?: number }>(event);
+
+ const omniMessageId = String(body?.omniMessageId ?? "").trim();
+ if (!omniMessageId) {
+ throw createError({ statusCode: 400, statusMessage: "omniMessageId is required" });
+ }
+
+ const msg = await prisma.omniMessage.findFirst({
+ where: { id: omniMessageId, teamId: auth.teamId, channel: "TELEGRAM", direction: "OUT" },
+ select: { id: true },
+ });
+ if (!msg) {
+ throw createError({ statusCode: 404, statusMessage: "telegram outbound message not found" });
+ }
+
+ const attempts = Math.max(1, Math.min(Number(body?.attempts ?? 12), 50));
+ const job = await enqueueTelegramSend({ omniMessageId }, { attempts });
+
+ return {
+ ok: true,
+ queue: "omni-outbound",
+ jobId: job.id,
+ omniMessageId,
+ };
+});
diff --git a/Frontend/server/api/pilot-chat.post.ts b/Frontend/server/api/pilot-chat.post.ts
new file mode 100644
index 0000000..8b12ae6
--- /dev/null
+++ b/Frontend/server/api/pilot-chat.post.ts
@@ -0,0 +1,130 @@
+import { readBody } from "h3";
+import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
+import { getAuthContext } from "../utils/auth";
+import { prisma } from "../utils/prisma";
+import { buildChangeSet, captureSnapshot } from "../utils/changeSet";
+import { persistChatMessage, runCrmAgentFor, type AgentTraceEvent } from "../agent/crmAgent";
+
+function extractMessageText(message: any): string {
+ if (!message || !Array.isArray(message.parts)) return "";
+ return message.parts
+ .filter((part: any) => part?.type === "text" && typeof part.text === "string")
+ .map((part: any) => part.text)
+ .join("")
+ .trim();
+}
+
+function getLastUserText(messages: any[]): string {
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
+ const message = messages[i];
+ if (message?.role !== "user") continue;
+ const text = extractMessageText(message);
+ if (text) return text;
+ }
+ return "";
+}
+
+function humanizeTraceText(trace: AgentTraceEvent): string {
+ if (trace.toolRun?.name) {
+ return `Использую инструмент: ${trace.toolRun.name}`;
+ }
+
+ const text = (trace.text ?? "").trim();
+ if (!text) return "Агент работает с данными CRM.";
+
+ if (text.toLowerCase().includes("ошиб")) return "Возникла ошибка шага, пробую другой путь.";
+ if (text.toLowerCase().includes("итог")) return "Готовлю финальный ответ.";
+ return text;
+}
+
+export default defineEventHandler(async (event) => {
+ const auth = await getAuthContext(event);
+ const body = await readBody<{ messages?: any[] }>(event);
+ const messages = Array.isArray(body?.messages) ? body.messages : [];
+ const userText = getLastUserText(messages);
+
+ if (!userText) {
+ throw createError({ statusCode: 400, statusMessage: "Last user message is required" });
+ }
+ const requestId = `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`;
+
+ const stream = createUIMessageStream({
+ execute: async ({ writer }) => {
+ const textId = `text-${Date.now()}`;
+ writer.write({ type: "start" });
+ try {
+ const snapshotBefore = await captureSnapshot(prisma, auth.teamId);
+
+ await persistChatMessage({
+ teamId: auth.teamId,
+ conversationId: auth.conversationId,
+ authorUserId: auth.userId,
+ role: "USER",
+ text: userText,
+ requestId,
+ eventType: "user",
+ phase: "final",
+ transient: false,
+ });
+
+ const reply = await runCrmAgentFor({
+ teamId: auth.teamId,
+ userId: auth.userId,
+ userText,
+ requestId,
+ conversationId: auth.conversationId,
+ onTrace: async (trace: AgentTraceEvent) => {
+ writer.write({
+ type: "data-agent-log",
+ data: {
+ requestId,
+ at: new Date().toISOString(),
+ text: humanizeTraceText(trace),
+ },
+ });
+ },
+ });
+
+ const snapshotAfter = await captureSnapshot(prisma, auth.teamId);
+ const changeSet = buildChangeSet(snapshotBefore, snapshotAfter);
+
+ await persistChatMessage({
+ teamId: auth.teamId,
+ conversationId: auth.conversationId,
+ authorUserId: null,
+ role: "ASSISTANT",
+ text: reply.text,
+ requestId,
+ eventType: "assistant",
+ phase: "final",
+ transient: false,
+ changeSet,
+ });
+
+ writer.write({ type: "text-start", id: textId });
+ writer.write({ type: "text-delta", id: textId, delta: reply.text });
+ writer.write({ type: "text-end", id: textId });
+ writer.write({ type: "finish", finishReason: "stop" });
+ } catch (error: any) {
+ writer.write({
+ type: "data-agent-log",
+ data: {
+ requestId,
+ at: new Date().toISOString(),
+ text: "Ошибка выполнения агентского цикла.",
+ },
+ });
+ writer.write({ type: "text-start", id: textId });
+ writer.write({
+ type: "text-delta",
+ id: textId,
+ delta: `Не удалось завершить задачу: ${String(error?.message ?? "unknown error")}`,
+ });
+ writer.write({ type: "text-end", id: textId });
+ writer.write({ type: "finish", finishReason: "stop" });
+ }
+ },
+ });
+
+ return createUIMessageStreamResponse({ stream });
+});
diff --git a/Frontend/server/api/pilot-transcribe.post.ts b/Frontend/server/api/pilot-transcribe.post.ts
new file mode 100644
index 0000000..1b28f32
--- /dev/null
+++ b/Frontend/server/api/pilot-transcribe.post.ts
@@ -0,0 +1,62 @@
+import { readBody } from "h3";
+import { getAuthContext } from "../utils/auth";
+import { transcribeWithWhisper } from "../utils/whisper";
+
+type TranscribeBody = {
+ audioBase64?: string;
+ sampleRate?: number;
+ language?: string;
+};
+
+function decodeBase64Pcm16(audioBase64: string) {
+ const pcmBuffer = Buffer.from(audioBase64, "base64");
+ if (pcmBuffer.length < 2) return new Float32Array();
+
+ const sampleCount = Math.floor(pcmBuffer.length / 2);
+ const out = new Float32Array(sampleCount);
+
+ for (let i = 0; i < sampleCount; i += 1) {
+ const lo = pcmBuffer[i * 2]!;
+ const hi = pcmBuffer[i * 2 + 1]!;
+ const int16 = (hi << 8) | lo;
+ const signed = int16 >= 0x8000 ? int16 - 0x10000 : int16;
+ out[i] = signed / 32768;
+ }
+
+ return out;
+}
+
+export default defineEventHandler(async (event) => {
+ await getAuthContext(event);
+
+ const body = await readBody(event);
+ const audioBase64 = String(body?.audioBase64 ?? "").trim();
+ const sampleRateRaw = Number(body?.sampleRate ?? 0);
+ const language = String(body?.language ?? "").trim() || undefined;
+
+ if (!audioBase64) {
+ throw createError({ statusCode: 400, statusMessage: "audioBase64 is required" });
+ }
+
+ if (!Number.isFinite(sampleRateRaw) || sampleRateRaw < 8000 || sampleRateRaw > 48000) {
+ throw createError({ statusCode: 400, statusMessage: "sampleRate must be between 8000 and 48000" });
+ }
+
+ const samples = decodeBase64Pcm16(audioBase64);
+ if (!samples.length) {
+ throw createError({ statusCode: 400, statusMessage: "Audio is empty" });
+ }
+
+ const maxSamples = Math.floor(sampleRateRaw * 120);
+ if (samples.length > maxSamples) {
+ throw createError({ statusCode: 413, statusMessage: "Audio is too long (max 120s)" });
+ }
+
+ const text = await transcribeWithWhisper({
+ samples,
+ sampleRate: sampleRateRaw,
+ language,
+ });
+
+ return { text };
+});
diff --git a/Frontend/server/graphql/schema.ts b/Frontend/server/graphql/schema.ts
index cd3631c..9ae4cc6 100644
--- a/Frontend/server/graphql/schema.ts
+++ b/Frontend/server/graphql/schema.ts
@@ -5,7 +5,6 @@ import { clearAuthSession, setSession } from "../utils/auth";
import { prisma } from "../utils/prisma";
import { normalizePhone, verifyPassword } from "../utils/password";
import { persistChatMessage, runCrmAgentFor } from "../agent/crmAgent";
-import type { AgentTraceEvent } from "../agent/crmAgent";
import { buildChangeSet, captureSnapshot, rollbackChangeSet } from "../utils/changeSet";
import type { ChangeSet } from "../utils/changeSet";
@@ -210,6 +209,55 @@ async function selectChatConversation(auth: AuthContext | null, event: H3Event,
return { ok: true };
}
+async function archiveChatConversation(auth: AuthContext | null, event: H3Event, id: string) {
+ const ctx = requireAuth(auth);
+ const convId = (id ?? "").trim();
+ if (!convId) throw new Error("id is required");
+
+ const conversation = await prisma.chatConversation.findFirst({
+ where: {
+ id: convId,
+ teamId: ctx.teamId,
+ createdByUserId: ctx.userId,
+ },
+ select: { id: true },
+ });
+
+ if (!conversation) throw new Error("conversation not found");
+
+ const nextConversationId = await prisma.$transaction(async (tx) => {
+ await tx.chatConversation.delete({ where: { id: conversation.id } });
+
+ if (ctx.conversationId !== conversation.id) {
+ return ctx.conversationId;
+ }
+
+ const fallback = await tx.chatConversation.findFirst({
+ where: { teamId: ctx.teamId, createdByUserId: ctx.userId },
+ orderBy: { updatedAt: "desc" },
+ select: { id: true },
+ });
+
+ if (fallback) {
+ return fallback.id;
+ }
+
+ const created = await tx.chatConversation.create({
+ data: { teamId: ctx.teamId, createdByUserId: ctx.userId, title: "Pilot" },
+ select: { id: true },
+ });
+ return created.id;
+ });
+
+ setSession(event, {
+ teamId: ctx.teamId,
+ userId: ctx.userId,
+ conversationId: nextConversationId,
+ });
+
+ return { ok: true };
+}
+
async function getChatMessages(auth: AuthContext | null) {
const ctx = requireAuth(auth);
const items = await prisma.chatMessage.findMany({
@@ -219,25 +267,18 @@ async function getChatMessages(auth: AuthContext | null) {
});
return items.map((m) => {
- const debug = (m.planJson as any) ?? {};
const cs = getChangeSetFromPlanJson(m.planJson);
return {
id: m.id,
role: m.role === "USER" ? "user" : m.role === "ASSISTANT" ? "assistant" : "system",
text: m.text,
- thinking: Array.isArray(debug.thinking) ? (debug.thinking as string[]) : [],
- tools: Array.isArray(debug.tools) ? (debug.tools as string[]) : [],
- toolRuns: Array.isArray(debug.toolRuns)
- ? (debug.toolRuns as any[])
- .filter((t) => t && typeof t === "object")
- .map((t: any) => ({
- name: String(t.name ?? "crm:unknown"),
- status: t.status === "error" ? "error" : "ok",
- input: String(t.input ?? ""),
- output: String(t.output ?? ""),
- at: t.at ? String(t.at) : m.createdAt.toISOString(),
- }))
- : [],
+ requestId: null,
+ eventType: null,
+ phase: null,
+ transient: null,
+ thinking: [],
+ tools: [],
+ toolRuns: [],
changeSetId: cs?.id ?? null,
changeStatus: cs?.status ?? null,
changeSummary: cs?.summary ?? null,
@@ -292,7 +333,10 @@ async function getDashboard(auth: AuthContext | null) {
}),
prisma.deal.findMany({
where: { teamId: ctx.teamId },
- include: { contact: { select: { name: true, company: true } } },
+ include: {
+ contact: { select: { name: true, company: true } },
+ steps: { orderBy: [{ order: "asc" }, { createdAt: "asc" }] },
+ },
orderBy: { updatedAt: "desc" },
take: 500,
}),
@@ -366,6 +410,16 @@ async function getDashboard(auth: AuthContext | null) {
amount: d.amount ? String(d.amount) : "",
nextStep: d.nextStep ?? "",
summary: d.summary ?? "",
+ currentStepId: d.currentStepId ?? "",
+ steps: d.steps.map((step) => ({
+ id: step.id,
+ title: step.title,
+ description: step.description ?? "",
+ status: step.status,
+ dueAt: step.dueAt?.toISOString() ?? "",
+ order: step.order,
+ completedAt: step.completedAt?.toISOString() ?? "",
+ })),
}));
const feed = feedRaw.map((c) => ({
@@ -596,6 +650,7 @@ async function sendPilotMessage(auth: AuthContext | null, textInput: string) {
const ctx = requireAuth(auth);
const text = (textInput ?? "").trim();
if (!text) throw new Error("text is required");
+ const requestId = `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`;
const snapshotBefore = await captureSnapshot(prisma, ctx.teamId);
@@ -605,24 +660,19 @@ async function sendPilotMessage(auth: AuthContext | null, textInput: string) {
authorUserId: ctx.userId,
role: "USER",
text,
+ requestId,
+ eventType: "user",
+ phase: "final",
+ transient: false,
});
const reply = await runCrmAgentFor({
teamId: ctx.teamId,
userId: ctx.userId,
userText: text,
- onTrace: async (event: AgentTraceEvent) => {
- await persistChatMessage({
- teamId: ctx.teamId,
- conversationId: ctx.conversationId,
- authorUserId: null,
- role: "SYSTEM",
- text: event.text,
- thinking: [],
- tools: event.toolRun ? [event.toolRun.name] : [],
- toolRuns: event.toolRun ? [event.toolRun] : [],
- });
- },
+ requestId,
+ conversationId: ctx.conversationId,
+ onTrace: async () => {},
});
const snapshotAfter = await captureSnapshot(prisma, ctx.teamId);
@@ -634,9 +684,10 @@ async function sendPilotMessage(auth: AuthContext | null, textInput: string) {
authorUserId: null,
role: "ASSISTANT",
text: reply.text,
- thinking: reply.thinking ?? [],
- tools: reply.tools,
- toolRuns: reply.toolRuns ?? [],
+ requestId,
+ eventType: "assistant",
+ phase: "final",
+ transient: false,
changeSet,
});
@@ -654,9 +705,6 @@ async function logPilotNote(auth: AuthContext | null, textInput: string) {
authorUserId: null,
role: "ASSISTANT",
text,
- thinking: [],
- tools: [],
- toolRuns: [],
});
return { ok: true };
@@ -675,6 +723,7 @@ export const crmGraphqlSchema = buildSchema(`
logout: MutationResult!
createChatConversation(title: String): Conversation!
selectChatConversation(id: ID!): MutationResult!
+ archiveChatConversation(id: ID!): MutationResult!
sendPilotMessage(text: String!): MutationResult!
confirmLatestChangeSet: MutationResult!
rollbackLatestChangeSet: MutationResult!
@@ -743,6 +792,10 @@ export const crmGraphqlSchema = buildSchema(`
id: ID!
role: String!
text: String!
+ requestId: String
+ eventType: String
+ phase: String
+ transient: Boolean
thinking: [String!]!
tools: [String!]!
toolRuns: [PilotToolRun!]!
@@ -822,6 +875,18 @@ export const crmGraphqlSchema = buildSchema(`
amount: String!
nextStep: String!
summary: String!
+ currentStepId: String!
+ steps: [DealStep!]!
+ }
+
+ type DealStep {
+ id: ID!
+ title: String!
+ description: String!
+ status: String!
+ dueAt: String!
+ order: Int!
+ completedAt: String!
}
type FeedCard {
@@ -878,6 +943,9 @@ export const crmGraphqlRoot = {
selectChatConversation: async (args: { id: string }, context: GraphQLContext) =>
selectChatConversation(context.auth, context.event, args.id),
+ archiveChatConversation: async (args: { id: string }, context: GraphQLContext) =>
+ archiveChatConversation(context.auth, context.event, args.id),
+
sendPilotMessage: async (args: { text: string }, context: GraphQLContext) =>
sendPilotMessage(context.auth, args.text),
diff --git a/Frontend/server/queues/outboundDelivery.ts b/Frontend/server/queues/outboundDelivery.ts
new file mode 100644
index 0000000..35da29a
--- /dev/null
+++ b/Frontend/server/queues/outboundDelivery.ts
@@ -0,0 +1,200 @@
+import { Queue, Worker, type JobsOptions } from "bullmq";
+import { prisma } from "../utils/prisma";
+import { getRedis } from "../utils/redis";
+
+export const OUTBOUND_DELIVERY_QUEUE_NAME = "omni-outbound";
+
+export type OutboundDeliveryJob = {
+ omniMessageId: string;
+ endpoint: string;
+ method?: "POST" | "PUT" | "PATCH";
+ headers?: Record;
+ payload: unknown;
+ timeoutMs?: number;
+ channel?: string;
+ provider?: string;
+};
+
+function ensureHttpUrl(value: string) {
+ const raw = (value ?? "").trim();
+ if (!raw) throw new Error("endpoint is required");
+ const parsed = new URL(raw);
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
+ throw new Error(`Unsupported endpoint protocol: ${parsed.protocol}`);
+ }
+ return parsed.toString();
+}
+
+function compactError(error: unknown) {
+ if (!error) return "unknown_error";
+ if (typeof error === "string") return error;
+ const anyErr = error as any;
+ return String(anyErr?.message ?? anyErr);
+}
+
+function extractProviderMessageId(body: unknown): string | null {
+ const obj = body as any;
+ if (!obj || typeof obj !== "object") return null;
+ const candidate =
+ obj?.message_id ??
+ obj?.messageId ??
+ obj?.id ??
+ obj?.result?.message_id ??
+ obj?.result?.id ??
+ null;
+ if (candidate == null) return null;
+ return String(candidate);
+}
+
+export function outboundDeliveryQueue() {
+ return new Queue(OUTBOUND_DELIVERY_QUEUE_NAME, {
+ connection: getRedis(),
+ defaultJobOptions: {
+ removeOnComplete: { count: 1000 },
+ removeOnFail: { count: 5000 },
+ },
+ });
+}
+
+export async function enqueueOutboundDelivery(input: OutboundDeliveryJob, opts?: JobsOptions) {
+ const endpoint = ensureHttpUrl(input.endpoint);
+ const q = outboundDeliveryQueue();
+
+ // Keep source message in pending before actual send starts.
+ await prisma.omniMessage.update({
+ where: { id: input.omniMessageId },
+ data: {
+ status: "PENDING",
+ rawJson: {
+ queue: {
+ queueName: OUTBOUND_DELIVERY_QUEUE_NAME,
+ enqueuedAt: new Date().toISOString(),
+ },
+ deliveryRequest: {
+ endpoint,
+ method: input.method ?? "POST",
+ channel: input.channel ?? null,
+ provider: input.provider ?? null,
+ payload: input.payload,
+ },
+ },
+ },
+ });
+
+ return q.add("deliver", { ...input, endpoint }, {
+ jobId: `omni:${input.omniMessageId}`,
+ attempts: 12,
+ backoff: { type: "exponential", delay: 1000 },
+ ...opts,
+ });
+}
+
+export function startOutboundDeliveryWorker() {
+ return new Worker(
+ OUTBOUND_DELIVERY_QUEUE_NAME,
+ async (job) => {
+ const msg = await prisma.omniMessage.findUnique({
+ where: { id: job.data.omniMessageId },
+ include: { thread: true },
+ });
+ if (!msg) return;
+
+ // Idempotency: if already sent/delivered, do not resend.
+ if ((msg.status === "SENT" || msg.status === "DELIVERED" || msg.status === "READ") && msg.providerMessageId) {
+ return;
+ }
+
+ const endpoint = ensureHttpUrl(job.data.endpoint);
+ const timeoutMs = Math.max(1000, Math.min(job.data.timeoutMs ?? 20000, 120000));
+ const method = job.data.method ?? "POST";
+ const headers: Record = {
+ "content-type": "application/json",
+ ...(job.data.headers ?? {}),
+ };
+
+ const requestStartedAt = new Date().toISOString();
+ try {
+ const response = await fetch(endpoint, {
+ method,
+ headers,
+ body: JSON.stringify(job.data.payload ?? {}),
+ signal: AbortSignal.timeout(timeoutMs),
+ });
+
+ const text = await response.text();
+ const responseBody = (() => {
+ try {
+ return JSON.parse(text);
+ } catch {
+ return text;
+ }
+ })();
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody)}`);
+ }
+
+ const providerMessageId = extractProviderMessageId(responseBody);
+ await prisma.omniMessage.update({
+ where: { id: msg.id },
+ data: {
+ status: "SENT",
+ providerMessageId,
+ rawJson: {
+ queue: {
+ queueName: OUTBOUND_DELIVERY_QUEUE_NAME,
+ completedAt: new Date().toISOString(),
+ attemptsMade: job.attemptsMade + 1,
+ },
+ deliveryRequest: {
+ endpoint,
+ method,
+ channel: job.data.channel ?? null,
+ provider: job.data.provider ?? null,
+ startedAt: requestStartedAt,
+ payload: job.data.payload ?? null,
+ },
+ deliveryResponse: {
+ status: response.status,
+ body: responseBody,
+ },
+ },
+ },
+ });
+ } catch (error) {
+ const isLastAttempt =
+ typeof job.opts.attempts === "number" && job.attemptsMade + 1 >= job.opts.attempts;
+
+ if (isLastAttempt) {
+ await prisma.omniMessage.update({
+ where: { id: msg.id },
+ data: {
+ status: "FAILED",
+ rawJson: {
+ queue: {
+ queueName: OUTBOUND_DELIVERY_QUEUE_NAME,
+ failedAt: new Date().toISOString(),
+ attemptsMade: job.attemptsMade + 1,
+ },
+ deliveryRequest: {
+ endpoint,
+ method,
+ channel: job.data.channel ?? null,
+ provider: job.data.provider ?? null,
+ startedAt: requestStartedAt,
+ payload: job.data.payload ?? null,
+ },
+ deliveryError: {
+ message: compactError(error),
+ },
+ },
+ },
+ });
+ }
+
+ throw error;
+ }
+ },
+ { connection: getRedis() },
+ );
+}
diff --git a/Frontend/server/queues/telegramSend.ts b/Frontend/server/queues/telegramSend.ts
index d92a0a8..0812c62 100644
--- a/Frontend/server/queues/telegramSend.ts
+++ b/Frontend/server/queues/telegramSend.ts
@@ -1,92 +1,43 @@
-import { Queue, Worker, JobsOptions } from "bullmq";
-import { getRedis } from "../utils/redis";
+import type { JobsOptions } from "bullmq";
import { prisma } from "../utils/prisma";
-import { telegramBotApi } from "../utils/telegram";
-
-export const TELEGRAM_SEND_QUEUE_NAME = "telegram:send";
+import { telegramApiBase, requireTelegramBotToken } from "../utils/telegram";
+import { enqueueOutboundDelivery, startOutboundDeliveryWorker } from "./outboundDelivery";
type TelegramSendJob = {
omniMessageId: string;
};
-export function telegramSendQueue() {
- return new Queue(TELEGRAM_SEND_QUEUE_NAME, {
- connection: getRedis(),
- defaultJobOptions: {
- removeOnComplete: { count: 1000 },
- removeOnFail: { count: 5000 },
- },
- });
-}
-
export async function enqueueTelegramSend(input: TelegramSendJob, opts?: JobsOptions) {
- const q = telegramSendQueue();
- return q.add("send", input, {
- jobId: input.omniMessageId, // idempotency
- attempts: 10,
- backoff: { type: "exponential", delay: 1000 },
- ...opts,
+ const msg = await prisma.omniMessage.findUnique({
+ where: { id: input.omniMessageId },
+ include: { thread: true },
});
-}
+ if (!msg) throw new Error(`omni message not found: ${input.omniMessageId}`);
+ if (msg.channel !== "TELEGRAM" || msg.direction !== "OUT") {
+ throw new Error(`Invalid omni message for telegram send: ${msg.id}`);
+ }
-export function startTelegramSendWorker() {
- return new Worker(
- TELEGRAM_SEND_QUEUE_NAME,
- async (job) => {
- const msg = await prisma.omniMessage.findUnique({
- where: { id: job.data.omniMessageId },
- include: { thread: true },
- });
- if (!msg) return;
+ const token = requireTelegramBotToken();
+ const endpoint = `${telegramApiBase()}/bot${token}/sendMessage`;
+ const payload = {
+ chat_id: msg.thread.externalChatId,
+ text: msg.text,
+ ...(msg.thread.businessConnectionId ? { business_connection_id: msg.thread.businessConnectionId } : {}),
+ };
- // Idempotency: if we already sent it, don't send twice.
- if (msg.status === "SENT" && msg.providerMessageId) return;
-
- if (msg.channel !== "TELEGRAM" || msg.direction !== "OUT") {
- throw new Error(`Invalid omni message for telegram send: ${msg.id}`);
- }
-
- const thread = msg.thread;
- const chatId = thread.externalChatId;
- const businessConnectionId = thread.businessConnectionId || undefined;
-
- try {
- const result = await telegramBotApi("sendMessage", {
- chat_id: chatId,
- text: msg.text,
- ...(businessConnectionId ? { business_connection_id: businessConnectionId } : {}),
- });
-
- const providerMessageId = result?.message_id != null ? String(result.message_id) : null;
- await prisma.omniMessage.update({
- where: { id: msg.id },
- data: {
- status: "SENT",
- providerMessageId: providerMessageId,
- rawJson: result,
- },
- });
- } catch (e: any) {
- const isLastAttempt =
- typeof job.opts.attempts === "number" && job.attemptsMade + 1 >= job.opts.attempts;
-
- if (isLastAttempt) {
- await prisma.omniMessage.update({
- where: { id: msg.id },
- data: {
- status: "FAILED",
- rawJson: {
- error: String(e?.message || e),
- attemptsMade: job.attemptsMade + 1,
- },
- },
- });
- }
-
- throw e;
- }
+ return enqueueOutboundDelivery(
+ {
+ omniMessageId: msg.id,
+ endpoint,
+ method: "POST",
+ payload,
+ provider: "telegram_business",
+ channel: "TELEGRAM",
},
- { connection: getRedis() },
+ opts,
);
}
+export function startTelegramSendWorker() {
+ return startOutboundDeliveryWorker();
+}
diff --git a/Frontend/server/queues/worker.ts b/Frontend/server/queues/worker.ts
new file mode 100644
index 0000000..f934624
--- /dev/null
+++ b/Frontend/server/queues/worker.ts
@@ -0,0 +1,35 @@
+import { startOutboundDeliveryWorker } from "./outboundDelivery";
+import { prisma } from "../utils/prisma";
+import { getRedis } from "../utils/redis";
+
+const worker = startOutboundDeliveryWorker();
+console.log("[delivery-worker] started queue omni:outbound");
+
+async function shutdown(signal: string) {
+ console.log(`[delivery-worker] shutting down by ${signal}`);
+ try {
+ await worker.close();
+ } catch {
+ // ignore shutdown errors
+ }
+ try {
+ const redis = getRedis();
+ await redis.quit();
+ } catch {
+ // ignore shutdown errors
+ }
+ try {
+ await prisma.$disconnect();
+ } catch {
+ // ignore shutdown errors
+ }
+ process.exit(0);
+}
+
+process.on("SIGINT", () => {
+ void shutdown("SIGINT");
+});
+process.on("SIGTERM", () => {
+ void shutdown("SIGTERM");
+});
+
diff --git a/Frontend/server/utils/langfuse.ts b/Frontend/server/utils/langfuse.ts
new file mode 100644
index 0000000..39da023
--- /dev/null
+++ b/Frontend/server/utils/langfuse.ts
@@ -0,0 +1,29 @@
+import { Langfuse } from "langfuse";
+
+let client: Langfuse | null = null;
+
+function isTruthy(value: string | undefined) {
+ const v = (value ?? "").trim().toLowerCase();
+ return v === "1" || v === "true" || v === "yes" || v === "on";
+}
+
+export function isLangfuseEnabled() {
+ const enabledRaw = process.env.LANGFUSE_ENABLED;
+ if (enabledRaw && !isTruthy(enabledRaw)) return false;
+ return Boolean((process.env.LANGFUSE_PUBLIC_KEY ?? "").trim() && (process.env.LANGFUSE_SECRET_KEY ?? "").trim());
+}
+
+export function getLangfuseClient() {
+ if (!isLangfuseEnabled()) return null;
+ if (client) return client;
+
+ client = new Langfuse({
+ publicKey: (process.env.LANGFUSE_PUBLIC_KEY ?? "").trim(),
+ secretKey: (process.env.LANGFUSE_SECRET_KEY ?? "").trim(),
+ baseUrl: (process.env.LANGFUSE_BASE_URL ?? "http://langfuse-web:3000").trim(),
+ enabled: true,
+ });
+
+ return client;
+}
+
diff --git a/Frontend/server/utils/whisper.ts b/Frontend/server/utils/whisper.ts
new file mode 100644
index 0000000..ffbb903
--- /dev/null
+++ b/Frontend/server/utils/whisper.ts
@@ -0,0 +1,53 @@
+type WhisperTranscribeInput = {
+ samples: Float32Array;
+ sampleRate: number;
+ language?: string;
+};
+let whisperPipelinePromise: Promise | null = null;
+let transformersPromise: Promise | null = null;
+
+function getWhisperModelId() {
+ return (process.env.CF_WHISPER_MODEL ?? "Xenova/whisper-small").trim() || "Xenova/whisper-small";
+}
+
+function getWhisperLanguage() {
+ const value = (process.env.CF_WHISPER_LANGUAGE ?? "ru").trim();
+ return value || "ru";
+}
+
+async function getWhisperPipeline() {
+ if (!transformersPromise) {
+ transformersPromise = import("@xenova/transformers");
+ }
+
+ const { env, pipeline } = await transformersPromise;
+
+ if (!whisperPipelinePromise) {
+ env.allowRemoteModels = true;
+ env.allowLocalModels = true;
+ env.cacheDir = "/app/.data/transformers";
+
+ const modelId = getWhisperModelId();
+ whisperPipelinePromise = pipeline("automatic-speech-recognition", modelId);
+ }
+
+ return whisperPipelinePromise;
+}
+
+export async function transcribeWithWhisper(input: WhisperTranscribeInput) {
+ const transcriber = (await getWhisperPipeline()) as any;
+ const result = await transcriber(
+ input.samples,
+ {
+ sampling_rate: input.sampleRate,
+ language: (input.language ?? getWhisperLanguage()) || "ru",
+ task: "transcribe",
+ chunk_length_s: 20,
+ stride_length_s: 5,
+ return_timestamps: false,
+ },
+ );
+
+ const text = String((result as any)?.text ?? "").trim();
+ return text;
+}
diff --git a/compose.yaml b/compose.yaml
index a385ae1..1da3a91 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -13,7 +13,7 @@ services:
ports:
- "3000:3000"
environment:
- DATABASE_URL: "file:../../.data/clientsflow-dev.db"
+ DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/clientsflow?schema=public"
REDIS_URL: "redis://redis:6379"
CF_AGENT_MODE: "langgraph"
OPENROUTER_API_KEY: "${OPENROUTER_API_KEY:-}"
@@ -22,6 +22,12 @@ services:
OPENROUTER_HTTP_REFERER: "${OPENROUTER_HTTP_REFERER:-}"
OPENROUTER_X_TITLE: "clientsflow"
OPENROUTER_REASONING_ENABLED: "${OPENROUTER_REASONING_ENABLED:-0}"
+ CF_WHISPER_MODEL: "${CF_WHISPER_MODEL:-Xenova/whisper-small}"
+ CF_WHISPER_LANGUAGE: "${CF_WHISPER_LANGUAGE:-ru}"
+ LANGFUSE_ENABLED: "${LANGFUSE_ENABLED:-true}"
+ LANGFUSE_BASE_URL: "${LANGFUSE_BASE_URL:-http://langfuse-web:3000}"
+ LANGFUSE_PUBLIC_KEY: "${LANGFUSE_PUBLIC_KEY:-pk-lf-local}"
+ LANGFUSE_SECRET_KEY: "${LANGFUSE_SECRET_KEY:-sk-lf-local}"
# Set this in your shell or a compose override:
# OPENROUTER_API_KEY: "..."
# GIGACHAT_AUTH_KEY: "..." (if you use GigaChat integration)
@@ -31,6 +37,28 @@ services:
"
depends_on:
- redis
+ - postgres
+ - langfuse-web
+
+ delivery-worker:
+ image: node:22-bookworm-slim
+ working_dir: /app/Frontend
+ volumes:
+ - ./Frontend:/app/Frontend
+ - clientsflow_data:/app/.data
+ - delivery_node_modules:/app/Frontend/node_modules
+ environment:
+ DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/clientsflow?schema=public"
+ REDIS_URL: "redis://redis:6379"
+ TELEGRAM_API_BASE: "${TELEGRAM_API_BASE:-https://api.telegram.org}"
+ TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN:-}"
+ command: >
+ bash -lc "
+ bash ./scripts/compose-worker.sh
+ "
+ depends_on:
+ - redis
+ - postgres
redis:
image: redis:7-alpine
@@ -39,9 +67,157 @@ services:
volumes:
- redis_data:/data
+ postgres:
+ image: postgres:16-alpine
+ ports:
+ - "5432:5432"
+ environment:
+ POSTGRES_DB: "clientsflow"
+ POSTGRES_USER: "postgres"
+ POSTGRES_PASSWORD: "postgres"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+
+ langfuse-worker:
+ image: docker.io/langfuse/langfuse-worker:3
+ restart: always
+ depends_on:
+ langfuse-postgres:
+ condition: service_healthy
+ langfuse-minio:
+ condition: service_healthy
+ langfuse-redis:
+ condition: service_healthy
+ langfuse-clickhouse:
+ condition: service_healthy
+ environment: &langfuse_env
+ NEXTAUTH_URL: "http://localhost:3001"
+ DATABASE_URL: "postgresql://langfuse:langfuse@langfuse-postgres:5432/langfuse"
+ SALT: "clientsflow-local-salt"
+ ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ TELEMETRY_ENABLED: "false"
+ CLICKHOUSE_MIGRATION_URL: "clickhouse://langfuse-clickhouse:9000"
+ CLICKHOUSE_URL: "http://langfuse-clickhouse:8123"
+ CLICKHOUSE_USER: "clickhouse"
+ CLICKHOUSE_PASSWORD: "clickhouse"
+ CLICKHOUSE_CLUSTER_ENABLED: "false"
+ LANGFUSE_S3_EVENT_UPLOAD_BUCKET: "langfuse"
+ LANGFUSE_S3_EVENT_UPLOAD_REGION: "auto"
+ LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: "minio"
+ LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: "miniosecret"
+ LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: "http://langfuse-minio:9000"
+ LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
+ LANGFUSE_S3_EVENT_UPLOAD_PREFIX: "events/"
+ LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: "langfuse"
+ LANGFUSE_S3_MEDIA_UPLOAD_REGION: "auto"
+ LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: "minio"
+ LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: "miniosecret"
+ LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: "http://langfuse-minio:9000"
+ LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
+ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: "media/"
+ REDIS_HOST: "langfuse-redis"
+ REDIS_PORT: "6379"
+ REDIS_AUTH: "langfuse-redis"
+ REDIS_TLS_ENABLED: "false"
+
+ langfuse-web:
+ image: docker.io/langfuse/langfuse:3
+ restart: always
+ depends_on:
+ langfuse-postgres:
+ condition: service_healthy
+ langfuse-minio:
+ condition: service_healthy
+ langfuse-redis:
+ condition: service_healthy
+ langfuse-clickhouse:
+ condition: service_healthy
+ ports:
+ - "3001:3000"
+ environment:
+ <<: *langfuse_env
+ NEXTAUTH_SECRET: "clientsflow-local-nextauth-secret"
+ LANGFUSE_INIT_ORG_ID: "org-clientsflow"
+ LANGFUSE_INIT_ORG_NAME: "Clientsflow Local"
+ LANGFUSE_INIT_PROJECT_ID: "proj-clientsflow"
+ LANGFUSE_INIT_PROJECT_NAME: "clientsflow"
+ LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-local"
+ LANGFUSE_INIT_PROJECT_SECRET_KEY: "sk-lf-local"
+ LANGFUSE_INIT_USER_EMAIL: "admin@clientsflow.local"
+ LANGFUSE_INIT_USER_NAME: "Local Admin"
+ LANGFUSE_INIT_USER_PASSWORD: "clientsflow-local-admin"
+
+ langfuse-clickhouse:
+ image: docker.io/clickhouse/clickhouse-server:latest
+ restart: always
+ user: "101:101"
+ environment:
+ CLICKHOUSE_DB: "default"
+ CLICKHOUSE_USER: "clickhouse"
+ CLICKHOUSE_PASSWORD: "clickhouse"
+ volumes:
+ - langfuse_clickhouse_data:/var/lib/clickhouse
+ - langfuse_clickhouse_logs:/var/log/clickhouse-server
+ healthcheck:
+ test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"]
+ interval: 5s
+ timeout: 5s
+ retries: 20
+ start_period: 5s
+
+ langfuse-minio:
+ image: cgr.dev/chainguard/minio:latest
+ restart: always
+ entrypoint: sh
+ command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
+ environment:
+ MINIO_ROOT_USER: "minio"
+ MINIO_ROOT_PASSWORD: "miniosecret"
+ volumes:
+ - langfuse_minio_data:/data
+ healthcheck:
+ test: ["CMD", "mc", "ready", "local"]
+ interval: 2s
+ timeout: 5s
+ retries: 15
+ start_period: 5s
+
+ langfuse-redis:
+ image: docker.io/redis:7-alpine
+ restart: always
+ command: ["redis-server", "--requirepass", "langfuse-redis", "--maxmemory-policy", "noeviction"]
+ healthcheck:
+ test: ["CMD-SHELL", "redis-cli -a langfuse-redis ping | grep PONG"]
+ interval: 3s
+ timeout: 5s
+ retries: 20
+ start_period: 5s
+
+ langfuse-postgres:
+ image: postgres:16-alpine
+ restart: always
+ environment:
+ POSTGRES_DB: "langfuse"
+ POSTGRES_USER: "langfuse"
+ POSTGRES_PASSWORD: "langfuse"
+ volumes:
+ - langfuse_postgres_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U langfuse -d langfuse"]
+ interval: 3s
+ timeout: 3s
+ retries: 20
+ start_period: 5s
+
volumes:
clientsflow_data:
frontend_node_modules:
+ delivery_node_modules:
frontend_nuxt:
frontend_output:
redis_data:
+ postgres_data:
+ langfuse_postgres_data:
+ langfuse_clickhouse_data:
+ langfuse_clickhouse_logs:
+ langfuse_minio_data: