chat bot 3
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.691.0",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@nestjs/mapped-types": "^2.0.6",
|
||||
"ansi-colors": "^4.1.3",
|
||||
"axios": "^1.7.7",
|
||||
|
||||
Generated
+347
@@ -14,6 +14,15 @@ importers:
|
||||
'@faker-js/faker':
|
||||
specifier: ^7.6.0
|
||||
version: 7.6.0
|
||||
'@google/generative-ai':
|
||||
specifier: ^0.21.0
|
||||
version: 0.21.0
|
||||
'@langchain/core':
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.79(openai@5.12.2(zod@3.25.76))
|
||||
'@langchain/google-genai':
|
||||
specifier: ^0.1.0
|
||||
version: 0.1.12(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))(zod@3.25.76)
|
||||
'@nestjs/mapped-types':
|
||||
specifier: ^2.0.6
|
||||
version: 2.0.6(@nestjs/common@10.4.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)
|
||||
@@ -77,6 +86,9 @@ importers:
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.2
|
||||
langchain:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.36(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))(@langchain/google-genai@0.1.12(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))(zod@3.25.76))(axios@1.7.7)(handlebars@4.7.8)(openai@5.12.2(zod@3.25.76))
|
||||
mongoose:
|
||||
specifier: ^8.8.1
|
||||
version: 8.8.1(@aws-sdk/credential-providers@3.691.0(@aws-sdk/client-sso-oidc@3.691.0(@aws-sdk/client-sts@3.691.0)))(socks@2.8.3)
|
||||
@@ -405,6 +417,9 @@ packages:
|
||||
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@cfworker/json-schema@4.1.1':
|
||||
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
|
||||
|
||||
'@colors/colors@1.6.0':
|
||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
@@ -651,6 +666,14 @@ packages:
|
||||
resolution: {integrity: sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==}
|
||||
engines: {node: '>=14.0.0', npm: '>=6.0.0'}
|
||||
|
||||
'@google/generative-ai@0.21.0':
|
||||
resolution: {integrity: sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@google/generative-ai@0.24.1':
|
||||
resolution: {integrity: sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -692,6 +715,28 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@langchain/core@0.3.79':
|
||||
resolution: {integrity: sha512-ZLAs5YMM5N2UXN3kExMglltJrKKoW7hs3KMZFlXUnD7a5DFKBYxPFMeXA4rT+uvTxuJRZPCYX0JKI5BhyAWx4A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@langchain/google-genai@0.1.12':
|
||||
resolution: {integrity: sha512-0Ea0E2g63ejCuormVxbuoyJQ5BYN53i2/fb6WP8bMKzyh+y43R13V8JqOtr3e/GmgNyv3ou/VeaZjx7KAvu/0g==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@langchain/core': '>=0.3.17 <0.4.0'
|
||||
|
||||
'@langchain/openai@0.6.16':
|
||||
resolution: {integrity: sha512-v9INBOjE0w6ZrUE7kP9UkRyNsV7daH7aPeSOsPEJ35044UI3udPHwNduQ8VmaOUsD26OvSdg1b1GDhrqWLMaRw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@langchain/core': '>=0.3.68 <0.4.0'
|
||||
|
||||
'@langchain/textsplitters@0.1.0':
|
||||
resolution: {integrity: sha512-djI4uw9rlkAb5iMhtLED+xJebDdAG935AdP4eRTB02R7OB/act55Bj9wsskhZsvuyQRpO4O1wQOp85s6T6GWmw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@langchain/core': '>=0.2.21 <0.4.0'
|
||||
|
||||
'@lukeed/csprng@1.1.0':
|
||||
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1071,6 +1116,9 @@ packages:
|
||||
'@types/range-parser@1.2.7':
|
||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||
|
||||
'@types/retry@0.12.0':
|
||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
|
||||
|
||||
@@ -1083,6 +1131,9 @@ packages:
|
||||
'@types/triple-beam@1.3.5':
|
||||
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
|
||||
|
||||
'@types/uuid@10.0.0':
|
||||
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
|
||||
|
||||
'@types/validator@13.12.2':
|
||||
resolution: {integrity: sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==}
|
||||
|
||||
@@ -1214,6 +1265,10 @@ packages:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ansi-styles@6.2.1:
|
||||
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1363,6 +1418,10 @@ packages:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
camelcase@6.3.0:
|
||||
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1486,6 +1545,9 @@ packages:
|
||||
console-control-strings@1.1.0:
|
||||
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||
|
||||
console-table-printer@2.15.0:
|
||||
resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==}
|
||||
|
||||
content-disposition@0.5.4:
|
||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1610,6 +1672,10 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decamelize@1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
@@ -1854,6 +1920,9 @@ packages:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
@@ -2335,6 +2404,9 @@ packages:
|
||||
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
|
||||
hasBin: true
|
||||
|
||||
js-tiktoken@1.0.21:
|
||||
resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -2368,6 +2440,10 @@ packages:
|
||||
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
||||
engines: {'0': node >= 0.2.0}
|
||||
|
||||
jsonpointer@5.0.1:
|
||||
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
@@ -2392,6 +2468,81 @@ packages:
|
||||
kuler@2.0.0:
|
||||
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
|
||||
|
||||
langchain@0.3.36:
|
||||
resolution: {integrity: sha512-PqC19KChFF0QlTtYDFgfEbIg+SCnCXox29G8tY62QWfj9bOW7ew2kgWmPw5qoHLOTKOdQPvXET20/1Pdq8vAtQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@langchain/anthropic': '*'
|
||||
'@langchain/aws': '*'
|
||||
'@langchain/cerebras': '*'
|
||||
'@langchain/cohere': '*'
|
||||
'@langchain/core': '>=0.3.58 <0.4.0'
|
||||
'@langchain/deepseek': '*'
|
||||
'@langchain/google-genai': '*'
|
||||
'@langchain/google-vertexai': '*'
|
||||
'@langchain/google-vertexai-web': '*'
|
||||
'@langchain/groq': '*'
|
||||
'@langchain/mistralai': '*'
|
||||
'@langchain/ollama': '*'
|
||||
'@langchain/xai': '*'
|
||||
axios: '*'
|
||||
cheerio: '*'
|
||||
handlebars: ^4.7.8
|
||||
peggy: ^3.0.2
|
||||
typeorm: '*'
|
||||
peerDependenciesMeta:
|
||||
'@langchain/anthropic':
|
||||
optional: true
|
||||
'@langchain/aws':
|
||||
optional: true
|
||||
'@langchain/cerebras':
|
||||
optional: true
|
||||
'@langchain/cohere':
|
||||
optional: true
|
||||
'@langchain/deepseek':
|
||||
optional: true
|
||||
'@langchain/google-genai':
|
||||
optional: true
|
||||
'@langchain/google-vertexai':
|
||||
optional: true
|
||||
'@langchain/google-vertexai-web':
|
||||
optional: true
|
||||
'@langchain/groq':
|
||||
optional: true
|
||||
'@langchain/mistralai':
|
||||
optional: true
|
||||
'@langchain/ollama':
|
||||
optional: true
|
||||
'@langchain/xai':
|
||||
optional: true
|
||||
axios:
|
||||
optional: true
|
||||
cheerio:
|
||||
optional: true
|
||||
handlebars:
|
||||
optional: true
|
||||
peggy:
|
||||
optional: true
|
||||
typeorm:
|
||||
optional: true
|
||||
|
||||
langsmith@0.3.82:
|
||||
resolution: {integrity: sha512-RTcxtRm0zp2lV+pMesMW7EZSsIlqN7OmR2F6sZ/sOFQwmcLVl+VErMPV4VkX4Sycs4/EIAFT5hpr36EqiHoikQ==}
|
||||
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
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2693,6 +2844,10 @@ packages:
|
||||
resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
mustache@4.2.0:
|
||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||
hasBin: true
|
||||
|
||||
mute-stream@0.0.8:
|
||||
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
|
||||
|
||||
@@ -2817,6 +2972,21 @@ packages:
|
||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
openai@5.12.2:
|
||||
resolution: {integrity: sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
openapi-types@12.1.3:
|
||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2829,6 +2999,10 @@ packages:
|
||||
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
p-finally@1.0.0:
|
||||
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2845,6 +3019,18 @@ packages:
|
||||
resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
p-queue@6.6.2:
|
||||
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-retry@4.6.2:
|
||||
resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-timeout@3.2.0:
|
||||
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
@@ -3022,6 +3208,10 @@ packages:
|
||||
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
retry@0.13.1:
|
||||
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
reusify@1.0.4:
|
||||
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
@@ -3133,6 +3323,9 @@ packages:
|
||||
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
simple-wcswidth@1.1.2:
|
||||
resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==}
|
||||
|
||||
slice-ansi@5.0.0:
|
||||
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3440,6 +3633,10 @@ packages:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
@@ -3573,6 +3770,14 @@ packages:
|
||||
resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
zod-to-json-schema@3.25.0:
|
||||
resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==}
|
||||
peerDependencies:
|
||||
zod: ^3.25 || ^4
|
||||
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
@@ -4183,6 +4388,8 @@ snapshots:
|
||||
|
||||
'@babel/helper-validator-identifier@7.25.9': {}
|
||||
|
||||
'@cfworker/json-schema@4.1.1': {}
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
|
||||
'@commitlint/cli@19.5.0(@types/node@20.17.6)(typescript@5.6.3)':
|
||||
@@ -4402,6 +4609,10 @@ snapshots:
|
||||
|
||||
'@faker-js/faker@7.6.0': {}
|
||||
|
||||
'@google/generative-ai@0.21.0': {}
|
||||
|
||||
'@google/generative-ai@0.24.1': {}
|
||||
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
@@ -4447,6 +4658,48 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76))':
|
||||
dependencies:
|
||||
'@cfworker/json-schema': 4.1.1
|
||||
ansi-styles: 5.2.0
|
||||
camelcase: 6.3.0
|
||||
decamelize: 1.2.0
|
||||
js-tiktoken: 1.0.21
|
||||
langsmith: 0.3.82(openai@5.12.2(zod@3.25.76))
|
||||
mustache: 4.2.0
|
||||
p-queue: 6.6.2
|
||||
p-retry: 4.6.2
|
||||
uuid: 10.0.0
|
||||
zod: 3.25.76
|
||||
zod-to-json-schema: 3.25.0(zod@3.25.76)
|
||||
transitivePeerDependencies:
|
||||
- '@opentelemetry/api'
|
||||
- '@opentelemetry/exporter-trace-otlp-proto'
|
||||
- '@opentelemetry/sdk-trace-base'
|
||||
- openai
|
||||
|
||||
'@langchain/google-genai@0.1.12(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@google/generative-ai': 0.24.1
|
||||
'@langchain/core': 0.3.79(openai@5.12.2(zod@3.25.76))
|
||||
zod-to-json-schema: 3.25.0(zod@3.25.76)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@langchain/openai@0.6.16(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))':
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.79(openai@5.12.2(zod@3.25.76))
|
||||
js-tiktoken: 1.0.21
|
||||
openai: 5.12.2(zod@3.25.76)
|
||||
zod: 3.25.76
|
||||
transitivePeerDependencies:
|
||||
- ws
|
||||
|
||||
'@langchain/textsplitters@0.1.0(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))':
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.79(openai@5.12.2(zod@3.25.76))
|
||||
js-tiktoken: 1.0.21
|
||||
|
||||
'@lukeed/csprng@1.1.0': {}
|
||||
|
||||
'@mapbox/node-pre-gyp@1.0.11':
|
||||
@@ -4966,6 +5219,8 @@ snapshots:
|
||||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
|
||||
'@types/retry@0.12.0': {}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
@@ -4984,6 +5239,8 @@ snapshots:
|
||||
|
||||
'@types/triple-beam@1.3.5': {}
|
||||
|
||||
'@types/uuid@10.0.0': {}
|
||||
|
||||
'@types/validator@13.12.2': {}
|
||||
|
||||
'@types/webidl-conversions@7.0.3': {}
|
||||
@@ -5140,6 +5397,8 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
ansi-styles@6.2.1: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
@@ -5334,6 +5593,8 @@ snapshots:
|
||||
|
||||
callsites@3.1.0: {}
|
||||
|
||||
camelcase@6.3.0: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -5465,6 +5726,10 @@ snapshots:
|
||||
|
||||
console-control-strings@1.1.0: {}
|
||||
|
||||
console-table-printer@2.15.0:
|
||||
dependencies:
|
||||
simple-wcswidth: 1.1.2
|
||||
|
||||
content-disposition@0.5.4:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@@ -5576,6 +5841,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
supports-color: 5.5.0
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
defaults@1.0.4:
|
||||
@@ -5909,6 +6176,8 @@ snapshots:
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
@@ -6442,6 +6711,10 @@ snapshots:
|
||||
|
||||
jiti@1.21.6: {}
|
||||
|
||||
js-tiktoken@1.0.21:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.1.0:
|
||||
@@ -6466,6 +6739,8 @@ snapshots:
|
||||
|
||||
jsonparse@1.3.1: {}
|
||||
|
||||
jsonpointer@5.0.1: {}
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
dependencies:
|
||||
jws: 3.2.2
|
||||
@@ -6500,6 +6775,42 @@ snapshots:
|
||||
|
||||
kuler@2.0.0: {}
|
||||
|
||||
langchain@0.3.36(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))(@langchain/google-genai@0.1.12(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))(zod@3.25.76))(axios@1.7.7)(handlebars@4.7.8)(openai@5.12.2(zod@3.25.76)):
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.79(openai@5.12.2(zod@3.25.76))
|
||||
'@langchain/openai': 0.6.16(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))
|
||||
'@langchain/textsplitters': 0.1.0(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))
|
||||
js-tiktoken: 1.0.21
|
||||
js-yaml: 4.1.0
|
||||
jsonpointer: 5.0.1
|
||||
langsmith: 0.3.82(openai@5.12.2(zod@3.25.76))
|
||||
openapi-types: 12.1.3
|
||||
p-retry: 4.6.2
|
||||
uuid: 10.0.0
|
||||
yaml: 2.5.1
|
||||
zod: 3.25.76
|
||||
optionalDependencies:
|
||||
'@langchain/google-genai': 0.1.12(@langchain/core@0.3.79(openai@5.12.2(zod@3.25.76)))(zod@3.25.76)
|
||||
axios: 1.7.7
|
||||
handlebars: 4.7.8
|
||||
transitivePeerDependencies:
|
||||
- '@opentelemetry/api'
|
||||
- '@opentelemetry/exporter-trace-otlp-proto'
|
||||
- '@opentelemetry/sdk-trace-base'
|
||||
- openai
|
||||
- ws
|
||||
|
||||
langsmith@0.3.82(openai@5.12.2(zod@3.25.76)):
|
||||
dependencies:
|
||||
'@types/uuid': 10.0.0
|
||||
chalk: 4.1.2
|
||||
console-table-printer: 2.15.0
|
||||
p-queue: 6.6.2
|
||||
semver: 7.6.3
|
||||
uuid: 10.0.0
|
||||
optionalDependencies:
|
||||
openai: 5.12.2(zod@3.25.76)
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -6813,6 +7124,8 @@ snapshots:
|
||||
type-is: 1.6.18
|
||||
xtend: 4.0.2
|
||||
|
||||
mustache@4.2.0: {}
|
||||
|
||||
mute-stream@0.0.8: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
@@ -6934,6 +7247,12 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-function: 5.0.1
|
||||
|
||||
openai@5.12.2(zod@3.25.76):
|
||||
optionalDependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
openapi-types@12.1.3: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -6957,6 +7276,8 @@ snapshots:
|
||||
|
||||
os-tmpdir@1.0.2: {}
|
||||
|
||||
p-finally@1.0.0: {}
|
||||
|
||||
p-limit@3.1.0:
|
||||
dependencies:
|
||||
yocto-queue: 0.1.0
|
||||
@@ -6973,6 +7294,20 @@ snapshots:
|
||||
dependencies:
|
||||
p-limit: 4.0.0
|
||||
|
||||
p-queue@6.6.2:
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
p-timeout: 3.2.0
|
||||
|
||||
p-retry@4.6.2:
|
||||
dependencies:
|
||||
'@types/retry': 0.12.0
|
||||
retry: 0.13.1
|
||||
|
||||
p-timeout@3.2.0:
|
||||
dependencies:
|
||||
p-finally: 1.0.0
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
@@ -7127,6 +7462,8 @@ snapshots:
|
||||
onetime: 7.0.0
|
||||
signal-exit: 4.1.0
|
||||
|
||||
retry@0.13.1: {}
|
||||
|
||||
reusify@1.0.4: {}
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
@@ -7251,6 +7588,8 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.6.3
|
||||
|
||||
simple-wcswidth@1.1.2: {}
|
||||
|
||||
slice-ansi@5.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.1
|
||||
@@ -7574,6 +7913,8 @@ snapshots:
|
||||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
v8-compile-cache-lib@3.0.1: {}
|
||||
@@ -7706,3 +8047,9 @@ snapshots:
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
yocto-queue@1.1.1: {}
|
||||
|
||||
zod-to-json-schema@3.25.0(zod@3.25.76):
|
||||
dependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
@@ -56,7 +56,6 @@ import { ChatMessageRepo, createChatMessageRepo } from "../modules/chat/reposito
|
||||
import { WsAuthService } from "../modules/chat/wsAuth.service";
|
||||
import { ChatbotService } from "../modules/chatbot/providers/chatbot.service";
|
||||
import { DataContextService } from "../modules/chatbot/providers/data-context.service";
|
||||
import { LangChainService } from "../modules/chatbot/providers/langchain.service";
|
||||
import { LLMService } from "../modules/chatbot/providers/llm.service";
|
||||
import { WebSocketAuthService } from "../modules/chatbot/providers/websocket-auth.service";
|
||||
import { ChatSessionRepository, createChatSessionRepository } from "../modules/chatbot/repositories/chat-session.repository";
|
||||
@@ -206,7 +205,6 @@ const containerModules = new AsyncContainerModule(async (bind) => {
|
||||
bind<CacheService>(IOCTYPES.CacheService).to(CacheService).inSingletonScope();
|
||||
bind<ChatbotService>(IOCTYPES.ChatbotService).to(ChatbotService).inSingletonScope();
|
||||
bind<LLMService>(IOCTYPES.ChatbotLLMService).to(LLMService).inSingletonScope();
|
||||
bind<LangChainService>(IOCTYPES.ChatbotLangChainService).to(LangChainService).inSingletonScope();
|
||||
bind<DataContextService>(IOCTYPES.ChatbotDataContextService).to(DataContextService).inSingletonScope();
|
||||
bind<WebSocketAuthService>(IOCTYPES.ChatbotWebSocketAuthService).to(WebSocketAuthService).inSingletonScope();
|
||||
// #endregion
|
||||
@@ -291,8 +289,8 @@ const containerModules = new AsyncContainerModule(async (bind) => {
|
||||
bind<AboutUsRepo>(IOCTYPES.AboutUsRepo).toDynamicValue(CreateAboutUsRepo).inSingletonScope();
|
||||
bind<SiteSettingRepo>(IOCTYPES.SiteSettingRepo).toDynamicValue(CreateSiteSettingRepo).inSingletonScope();
|
||||
bind<NewsletterRepo>(IOCTYPES.NewsletterRepo).toDynamicValue(CreateNewsletterRepo).inSingletonScope();
|
||||
bind<ChatSessionRepository>(IOCTYPES.ChatSessionRepository).toDynamicValue(createChatSessionRepository).inSingletonScope();
|
||||
bind<ChatMessageRepository>(IOCTYPES.ChatMessageRepository).toDynamicValue(createChatMessageRepository).inSingletonScope();
|
||||
bind<ChatSessionRepository>(IOCTYPES.ChatbotChatSessionRepository).toDynamicValue(createChatSessionRepository).inSingletonScope();
|
||||
bind<ChatMessageRepository>(IOCTYPES.ChatbotChatMessageRepository).toDynamicValue(createChatMessageRepository).inSingletonScope();
|
||||
// #endregion
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ export const IOCTYPES = {
|
||||
OrderQueue: Symbol.for("OrderQueue"),
|
||||
ChatbotService: Symbol.for("ChatbotService"),
|
||||
ChatbotLLMService: Symbol.for("ChatbotLLMService"),
|
||||
ChatbotLangChainService: Symbol.for("ChatbotLangChainService"),
|
||||
ChatbotDataContextService: Symbol.for("ChatbotDataContextService"),
|
||||
ChatbotGateway: Symbol.for("ChatbotGateway"),
|
||||
ChatbotWebSocketAuthService: Symbol.for("ChatbotWebSocketAuthService"),
|
||||
@@ -137,8 +136,8 @@ export const IOCTYPES = {
|
||||
ContactUsRepo: Symbol.for("ContactUsRepo"),
|
||||
AboutUsRepo: Symbol.for("AboutUsRepo"),
|
||||
NewsletterRepo: Symbol.for("NewsletterRepo"),
|
||||
ChatSessionRepository: Symbol.for("ChatSessionRepository"),
|
||||
ChatMessageRepository: Symbol.for("ChatMessageRepository"),
|
||||
ChatbotChatSessionRepository: Symbol.for("ChatbotChatSessionRepository"),
|
||||
ChatbotChatMessageRepository: Symbol.for("ChatbotChatMessageRepository"),
|
||||
// #endregion
|
||||
Logger: Symbol.for("Logger"),
|
||||
ZarinPalGateway: Symbol.for("ZarinPalGateway"),
|
||||
|
||||
@@ -329,3 +329,17 @@ export const enum RoleMessage {
|
||||
PermissionsNotEmpty = "دسترسیها نباید خالی باشد",
|
||||
RoleExist = "این نقش قبلا ثبت شده است",
|
||||
}
|
||||
|
||||
export const enum WebSocketMessage {
|
||||
AUTHENTICATED = "احراز هویت موفقیتآمیز",
|
||||
AUTHENTICATION_REQUIRED = "احراز هویت ضروری است",
|
||||
INVALID_TOKEN = "توکن احراز هویت نامعتبر است",
|
||||
TOKEN_EXPIRED = "توکن منقضی شده است",
|
||||
USER_NOT_AUTHENTICATED = "کاربر احراز هویت نشده است",
|
||||
CONNECTION_FAILED = "اتصال برقرار نشد",
|
||||
SESSION_CREATED = "جلسه چت با موفقیت ایجاد شد",
|
||||
SESSION_CREATION_FAILED = "ایجاد جلسه چت با شکست مواجه شد",
|
||||
SESSION_NOT_FOUND = "جلسه چت یافت نشد",
|
||||
CHAT_JOINED = "با موفقیت به چت متصل شدید",
|
||||
LLM_SERVICE_ERROR = "سرویس هوش مصنوعی موقتاً در دسترس نیست",
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsOptional, IsString, MaxLength } from "class-validator";
|
||||
import { isValidObjectId } from "mongoose";
|
||||
|
||||
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||
import { IsValidId } from "../../../common/decorator/validation.decorator";
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { isValidObjectId } from "mongoose";
|
||||
|
||||
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||
import { CommonMessage } from "../../../common/enums/message.enum";
|
||||
import { IsValidId } from "../../../common/decorator/validation.decorator";
|
||||
import { ChatSessionModel } from "../models/chat-session.model";
|
||||
|
||||
export class SessionIdParamDto {
|
||||
@Expose()
|
||||
@IsNotEmpty({ message: CommonMessage.SESSION_ID_REQUIRED || "Session ID is required" })
|
||||
@IsNotEmpty({ message: "Session ID is required" })
|
||||
@IsValidId([ChatSessionModel])
|
||||
@ApiProperty({ type: "string", description: "Session id of the entity", example: "66eff8c0c6ad5530b996c432" })
|
||||
sessionId!: string;
|
||||
|
||||
@@ -53,8 +53,8 @@ class ChatbotController extends BaseController {
|
||||
return this.response({ data });
|
||||
}
|
||||
|
||||
@ApiOperation("Send a message in a chat session (Enhanced with LangChain + Fallback)")
|
||||
@ApiResponse("Message sent successfully with enhanced AI", HttpStatus.Created)
|
||||
@ApiOperation("Send a message in a chat session")
|
||||
@ApiResponse("Message sent successfully", HttpStatus.Created)
|
||||
@ApiModel(SendMessageDto)
|
||||
@ApiAuth()
|
||||
@httpPost("/messages", Guard.authUser(), ValidationMiddleware.validateInput(SendMessageDto))
|
||||
@@ -64,8 +64,8 @@ class ChatbotController extends BaseController {
|
||||
return this.response({ data }, HttpStatus.Created);
|
||||
}
|
||||
|
||||
@ApiOperation("Send a message and get streaming response (Enhanced with LangChain + Fallback)")
|
||||
@ApiResponse("Enhanced streaming response initiated")
|
||||
@ApiOperation("Send a message and get streaming response")
|
||||
@ApiResponse("Streaming response initiated")
|
||||
@ApiModel(SendMessageDto)
|
||||
@ApiAuth()
|
||||
@httpPost("/messages/stream", Guard.authUser(), ValidationMiddleware.validateInput(SendMessageDto))
|
||||
@@ -76,7 +76,6 @@ class ChatbotController extends BaseController {
|
||||
): Promise<void> {
|
||||
const user = req.user as IUser;
|
||||
try {
|
||||
// Now uses LangChain by default with automatic Gemini fallback
|
||||
const { userMessage, streamGenerator } = await this.chatbotService.sendMessageStream(user._id.toString(), sendDto);
|
||||
|
||||
// Set headers for Server-Sent Events
|
||||
@@ -86,10 +85,10 @@ class ChatbotController extends BaseController {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
// Send initial user message
|
||||
res.write(`data: ${JSON.stringify({ type: "user_message", data: userMessage, enhanced: true })}\n\n`);
|
||||
res.write(`data: ${JSON.stringify({ type: "user_message", data: userMessage })}\n\n`);
|
||||
|
||||
// Start streaming bot response
|
||||
res.write(`data: ${JSON.stringify({ type: "bot_response_start", provider: "enhanced" })}\n\n`);
|
||||
res.write(`data: ${JSON.stringify({ type: "bot_response_start" })}\n\n`);
|
||||
|
||||
const stream = await streamGenerator();
|
||||
for await (const chunk of stream) {
|
||||
@@ -104,20 +103,12 @@ class ChatbotController extends BaseController {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.write(
|
||||
`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. LangChain با خطا مواجه شد و به Gemini بازگشت ⚠️" })}\n\n`,
|
||||
`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. لطفاً دوباره تلاش کنید ⚠️" })}\n\n`,
|
||||
);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOperation("Refresh LangChain training data from database")
|
||||
@ApiResponse("Training data refreshed successfully")
|
||||
@ApiAuth()
|
||||
@httpPost("/training/refresh", Guard.authUser())
|
||||
public async refreshTrainingData(@request() _req: Request) {
|
||||
const data = await this.chatbotService.refreshLangChainData();
|
||||
return this.response({ data });
|
||||
}
|
||||
|
||||
@ApiOperation("Close a chat session")
|
||||
@ApiResponse("Chat session closed successfully")
|
||||
|
||||
@@ -1,539 +0,0 @@
|
||||
import { Logger, UseFilters, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
||||
import {
|
||||
ConnectedSocket,
|
||||
MessageBody,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
OnGatewayInit,
|
||||
SubscribeMessage,
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
} from "@nestjs/websockets";
|
||||
import { Server, Socket } from "socket.io";
|
||||
|
||||
import { WEBSOCKET_EVENTS } from "./constants/chatbot.constants";
|
||||
import { AuthenticateDto, CreateSessionDto, JoinChatDto, LeaveChatDto, SendMessageWebSocketDto, TypingDto } from "./DTO/websocket-events.dto";
|
||||
import { WebSocketAuthGuard } from "./guards/websocket-auth.guard";
|
||||
import { AuthenticatedSocket, ConnectedUserInfo, WebSocketResponse } from "./interfaces/websocket.interface";
|
||||
import { ChatbotService } from "./providers/chatbot.service";
|
||||
import { WebSocketAuthService } from "./providers/websocket-auth.service";
|
||||
import { WebSocketMessage } from "../../common/enums/message.enum";
|
||||
import { WsExceptionFilter } from "../../core/filters/ws-exception.filter";
|
||||
|
||||
@WebSocketGateway({
|
||||
cors: { origin: "*", credentials: true },
|
||||
namespace: "/chat",
|
||||
})
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@UseFilters(WsExceptionFilter)
|
||||
export class ChatbotGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
|
||||
@WebSocketServer()
|
||||
server: Server;
|
||||
|
||||
private readonly logger = new Logger(ChatbotGateway.name);
|
||||
|
||||
// Connection state management
|
||||
private readonly connectedUsers = new Map<string, ConnectedUserInfo>();
|
||||
private readonly userSessions = new Map<string, Set<string>>();
|
||||
|
||||
constructor(
|
||||
private readonly chatbotService: ChatbotService,
|
||||
private readonly authService: WebSocketAuthService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gateway initialization lifecycle hook
|
||||
*/
|
||||
afterInit(server: Server): void {
|
||||
this.logger.log("WebSocket Gateway initialized", {
|
||||
namespace: "/chat",
|
||||
cors: true,
|
||||
serverInstance: !!server,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles new WebSocket connections with authentication
|
||||
* Implements fail-fast principle for security
|
||||
*/
|
||||
async handleConnection(client: Socket): Promise<void> {
|
||||
const connectionContext = {
|
||||
clientId: client.id,
|
||||
clientIP: client.handshake.address,
|
||||
userAgent: client.handshake.headers["user-agent"],
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.logger.log("New WebSocket connection attempt", connectionContext);
|
||||
|
||||
try {
|
||||
const authResult = await this.authService.authenticateClient(client);
|
||||
|
||||
if (!authResult.success) {
|
||||
this.authService.handleAuthenticationFailure(client, authResult.error!);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = authResult.user!;
|
||||
this.registerUserConnection(client.id, user.id);
|
||||
this.authService.emitAuthenticationSuccess(client, user);
|
||||
|
||||
this.logger.log("WebSocket connection established", {
|
||||
...connectionContext,
|
||||
userId: user.id,
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("WebSocket connection error", {
|
||||
...connectionContext,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
this.authService.handleAuthenticationFailure(client, WebSocketMessage.CONNECTION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles client disconnections with cleanup
|
||||
*/
|
||||
handleDisconnect(client: Socket): void {
|
||||
const userInfo = this.connectedUsers.get(client.id);
|
||||
|
||||
this.logger.log("WebSocket client disconnected", {
|
||||
clientId: client.id,
|
||||
userId: userInfo?.userId,
|
||||
sessionId: userInfo?.sessionId,
|
||||
});
|
||||
|
||||
if (userInfo) {
|
||||
this.cleanupUserConnection(client, userInfo);
|
||||
}
|
||||
|
||||
this.connectedUsers.delete(client.id);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MESSAGE HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handles explicit authentication requests (for re-authentication)
|
||||
*/
|
||||
@SubscribeMessage(WEBSOCKET_EVENTS.AUTHENTICATE)
|
||||
async handleAuthenticate(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() _data: AuthenticateDto): Promise<void> {
|
||||
try {
|
||||
if (!this.authService.isClientAuthenticated(client)) {
|
||||
throw new Error(WebSocketMessage.USER_NOT_AUTHENTICATED);
|
||||
}
|
||||
|
||||
this.emitSuccessResponse(client, WEBSOCKET_EVENTS.AUTHENTICATED, {
|
||||
message: WebSocketMessage.AUTHENTICATED,
|
||||
userId: client.data.user.id,
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleMessageError(client, error, WEBSOCKET_EVENTS.AUTHENTICATE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles chat session creation
|
||||
*/
|
||||
@SubscribeMessage(WEBSOCKET_EVENTS.CREATE_SESSION)
|
||||
@UseGuards(WebSocketAuthGuard)
|
||||
async handleCreateSession(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: CreateSessionDto): Promise<void> {
|
||||
try {
|
||||
const userId = this.authService.getUserId(client);
|
||||
if (!userId) {
|
||||
throw new Error(WebSocketMessage.USER_NOT_AUTHENTICATED);
|
||||
}
|
||||
|
||||
const session = await this.chatbotService.createChatSession(userId, { title: data.title });
|
||||
|
||||
this.emitSuccessResponse(client, WEBSOCKET_EVENTS.SESSION_CREATED, {
|
||||
message: WebSocketMessage.SESSION_CREATED,
|
||||
session,
|
||||
});
|
||||
|
||||
this.logger.log("Chat session created", {
|
||||
sessionId: session.id,
|
||||
userId,
|
||||
title: data.title,
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleMessageError(client, error, WEBSOCKET_EVENTS.SESSION_ERROR);
|
||||
this.emitErrorResponse(client, WEBSOCKET_EVENTS.SESSION_ERROR, {
|
||||
message: WebSocketMessage.SESSION_CREATION_FAILED,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles joining a chat session
|
||||
*/
|
||||
@SubscribeMessage(WEBSOCKET_EVENTS.JOIN_CHAT)
|
||||
@UseGuards(WebSocketAuthGuard)
|
||||
async handleJoinChat(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: JoinChatDto): Promise<void> {
|
||||
try {
|
||||
const userId = this.authService.getUserId(client);
|
||||
if (!userId) {
|
||||
throw new Error(WebSocketMessage.USER_NOT_AUTHENTICATED);
|
||||
}
|
||||
|
||||
// Verify session ownership
|
||||
await this.chatbotService.getChatSession(data.sessionId, userId);
|
||||
|
||||
// Leave previous session if any
|
||||
await this.leavePreviousSession(client);
|
||||
|
||||
// Join new session
|
||||
await client.join(`session_${data.sessionId}`);
|
||||
this.updateUserSession(client.id, userId, data.sessionId);
|
||||
|
||||
// Emit success events
|
||||
this.emitSuccessResponse(client, WEBSOCKET_EVENTS.CHAT_JOINED, {
|
||||
message: WebSocketMessage.CHAT_JOINED,
|
||||
sessionId: data.sessionId,
|
||||
});
|
||||
|
||||
// Notify others in session
|
||||
client.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.USER_JOINED, {
|
||||
userId,
|
||||
sessionId: data.sessionId,
|
||||
});
|
||||
|
||||
this.logger.log("User joined chat session", {
|
||||
userId,
|
||||
sessionId: data.sessionId,
|
||||
clientId: client.id,
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleMessageError(client, error, WEBSOCKET_EVENTS.JOIN_CHAT);
|
||||
this.emitErrorResponse(client, WEBSOCKET_EVENTS.ERROR, {
|
||||
message: WebSocketMessage.SESSION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles leaving a chat session
|
||||
*/
|
||||
@SubscribeMessage(WEBSOCKET_EVENTS.LEAVE_CHAT)
|
||||
@UseGuards(WebSocketAuthGuard)
|
||||
async handleLeaveChat(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: LeaveChatDto): Promise<void> {
|
||||
try {
|
||||
const userId = this.authService.getUserId(client);
|
||||
if (!userId) {
|
||||
throw new Error(WebSocketMessage.USER_NOT_AUTHENTICATED);
|
||||
}
|
||||
|
||||
await client.leave(`session_${data.sessionId}`);
|
||||
this.clearUserSession(client.id);
|
||||
|
||||
// Emit success events
|
||||
this.emitSuccessResponse(client, WEBSOCKET_EVENTS.CHAT_LEFT, {
|
||||
sessionId: data.sessionId,
|
||||
});
|
||||
|
||||
// Notify others in session
|
||||
client.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.USER_LEFT, {
|
||||
userId,
|
||||
sessionId: data.sessionId,
|
||||
});
|
||||
|
||||
this.logger.log("User left chat session", {
|
||||
userId,
|
||||
sessionId: data.sessionId,
|
||||
clientId: client.id,
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleMessageError(client, error, WEBSOCKET_EVENTS.LEAVE_CHAT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles sending messages in chat
|
||||
*/
|
||||
@SubscribeMessage(WEBSOCKET_EVENTS.SEND_MESSAGE)
|
||||
@UseGuards(WebSocketAuthGuard)
|
||||
async handleSendMessage(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: SendMessageWebSocketDto): Promise<void> {
|
||||
try {
|
||||
const userId = this.authService.getUserId(client);
|
||||
if (!userId) {
|
||||
throw new Error(WebSocketMessage.USER_NOT_AUTHENTICATED);
|
||||
}
|
||||
|
||||
// Start typing indicator
|
||||
this.emitTypingIndicator(data.sessionId, "bot", true);
|
||||
|
||||
// Send message through service
|
||||
const userMessage = await this.chatbotService.sendMessage(userId, {
|
||||
sessionId: data.sessionId,
|
||||
content: data.content,
|
||||
responseToId: data.responseToId,
|
||||
metadata: data.metadata,
|
||||
});
|
||||
|
||||
// Emit user message confirmation
|
||||
this.server.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.MESSAGE_RECEIVED, {
|
||||
message: userMessage,
|
||||
sessionId: data.sessionId,
|
||||
enhanced: true,
|
||||
});
|
||||
|
||||
this.logger.log("Message sent", {
|
||||
userId,
|
||||
sessionId: data.sessionId,
|
||||
messageId: userMessage.id,
|
||||
});
|
||||
|
||||
// Generate bot response asynchronously
|
||||
this.generateBotResponseAsync(data.sessionId, userId, data.content);
|
||||
} catch (error) {
|
||||
this.handleMessageError(client, error, WEBSOCKET_EVENTS.MESSAGE_ERROR);
|
||||
this.emitErrorResponse(client, WEBSOCKET_EVENTS.MESSAGE_ERROR, {
|
||||
message: WebSocketMessage.LLM_SERVICE_ERROR,
|
||||
});
|
||||
this.emitTypingIndicator(data.sessionId, "bot", false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles typing start events
|
||||
*/
|
||||
@SubscribeMessage(WEBSOCKET_EVENTS.TYPING_START)
|
||||
@UseGuards(WebSocketAuthGuard)
|
||||
async handleTypingStart(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: TypingDto): Promise<void> {
|
||||
const userId = this.authService.getUserId(client);
|
||||
if (!userId) return;
|
||||
|
||||
client.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.TYPING_START, {
|
||||
userId,
|
||||
type: "user",
|
||||
sessionId: data.sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles typing stop events
|
||||
*/
|
||||
@SubscribeMessage(WEBSOCKET_EVENTS.TYPING_STOP)
|
||||
@UseGuards(WebSocketAuthGuard)
|
||||
async handleTypingStop(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: TypingDto): Promise<void> {
|
||||
const userId = this.authService.getUserId(client);
|
||||
if (!userId) return;
|
||||
|
||||
client.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.TYPING_STOP, {
|
||||
userId,
|
||||
type: "user",
|
||||
sessionId: data.sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PRIVATE HELPER METHODS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Registers a new user connection
|
||||
*/
|
||||
private registerUserConnection(clientId: string, userId: string): void {
|
||||
this.connectedUsers.set(clientId, { userId });
|
||||
|
||||
if (!this.userSessions.has(userId)) {
|
||||
this.userSessions.set(userId, new Set());
|
||||
}
|
||||
this.userSessions.get(userId)!.add(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates user session information
|
||||
*/
|
||||
private updateUserSession(clientId: string, _userId: string, sessionId: string): void {
|
||||
const userInfo = this.connectedUsers.get(clientId);
|
||||
if (userInfo) {
|
||||
userInfo.sessionId = sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears user session information
|
||||
*/
|
||||
private clearUserSession(clientId: string): void {
|
||||
const userInfo = this.connectedUsers.get(clientId);
|
||||
if (userInfo) {
|
||||
userInfo.sessionId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up user connection on disconnect
|
||||
*/
|
||||
private cleanupUserConnection(client: Socket, userInfo: ConnectedUserInfo): void {
|
||||
// Remove from user sessions
|
||||
const userSockets = this.userSessions.get(userInfo.userId);
|
||||
if (userSockets) {
|
||||
userSockets.delete(client.id);
|
||||
if (userSockets.size === 0) {
|
||||
this.userSessions.delete(userInfo.userId);
|
||||
}
|
||||
}
|
||||
|
||||
// Leave session room if joined
|
||||
if (userInfo.sessionId) {
|
||||
client.leave(`session_${userInfo.sessionId}`);
|
||||
|
||||
// Notify others in session
|
||||
this.server.to(`session_${userInfo.sessionId}`).emit(WEBSOCKET_EVENTS.USER_LEFT, {
|
||||
userId: userInfo.userId,
|
||||
sessionId: userInfo.sessionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles leaving previous session when joining a new one
|
||||
*/
|
||||
private async leavePreviousSession(client: Socket): Promise<void> {
|
||||
const userInfo = this.connectedUsers.get(client.id);
|
||||
if (userInfo?.sessionId) {
|
||||
await client.leave(`session_${userInfo.sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits typing indicators
|
||||
*/
|
||||
private emitTypingIndicator(sessionId: string, type: "user" | "bot", isTyping: boolean): void {
|
||||
const event = isTyping ? WEBSOCKET_EVENTS.TYPING_START : WEBSOCKET_EVENTS.TYPING_STOP;
|
||||
|
||||
this.server.to(`session_${sessionId}`).emit(event, {
|
||||
type,
|
||||
sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates bot response asynchronously
|
||||
*/
|
||||
private async generateBotResponseAsync(sessionId: string, userId: string, userMessage: string): Promise<void> {
|
||||
try {
|
||||
// Start bot response
|
||||
this.server.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.BOT_RESPONSE_START, {
|
||||
sessionId,
|
||||
provider: "enhanced",
|
||||
});
|
||||
|
||||
// Get streaming response
|
||||
const { streamGenerator } = await this.chatbotService.sendMessageStream(userId, {
|
||||
sessionId,
|
||||
content: userMessage,
|
||||
});
|
||||
|
||||
const stream = await streamGenerator();
|
||||
|
||||
// Stream chunks to client
|
||||
for await (const chunk of stream) {
|
||||
if (chunk) {
|
||||
this.server.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.BOT_RESPONSE_CHUNK, {
|
||||
data: chunk,
|
||||
sessionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// End bot response
|
||||
this.server.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.BOT_RESPONSE_END, { sessionId });
|
||||
|
||||
this.emitTypingIndicator(sessionId, "bot", false);
|
||||
|
||||
this.logger.log("Bot response completed", { sessionId, userId });
|
||||
} catch (error) {
|
||||
this.logger.error("Bot response generation failed", {
|
||||
sessionId,
|
||||
userId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
this.server.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.ERROR, {
|
||||
message: WebSocketMessage.LLM_SERVICE_ERROR,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
this.emitTypingIndicator(sessionId, "bot", false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles message-level errors with proper logging
|
||||
*/
|
||||
private handleMessageError(client: Socket, error: unknown, event: WEBSOCKET_EVENTS): void {
|
||||
const errorContext = this.authService.createErrorContext(client, event);
|
||||
|
||||
this.logger.error(`WebSocket message error: ${event}`, {
|
||||
...errorContext,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits success response with consistent structure
|
||||
*/
|
||||
private emitSuccessResponse<T>(client: Socket, event: WEBSOCKET_EVENTS, data: T): void {
|
||||
const response: WebSocketResponse<T> = {
|
||||
...data,
|
||||
status: "success",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
console.log(data);
|
||||
|
||||
client.emit(event, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits error response with consistent structure
|
||||
*/
|
||||
private emitErrorResponse(client: Socket, event: WEBSOCKET_EVENTS, error: { message: string }): void {
|
||||
const response: WebSocketResponse = {
|
||||
status: "error",
|
||||
message: error.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
client.emit(event, response);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PUBLIC UTILITY METHODS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Public method to emit bot responses (called from service)
|
||||
*/
|
||||
async emitBotResponse(sessionId: string, message: unknown): Promise<void> {
|
||||
this.server.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.BOT_RESPONSE, {
|
||||
message,
|
||||
sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets connected users in a session
|
||||
*/
|
||||
getSessionUsers(sessionId: string): string[] {
|
||||
const users: string[] = [];
|
||||
this.connectedUsers.forEach((userInfo) => {
|
||||
if (userInfo.sessionId === sessionId) {
|
||||
users.push(userInfo.userId);
|
||||
}
|
||||
});
|
||||
return [...new Set(users)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user is online
|
||||
*/
|
||||
isUserOnline(userId: string): boolean {
|
||||
return this.userSessions.has(userId) && this.userSessions.get(userId)!.size > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { ChatbotController } from "./chatbot.controller";
|
||||
import { ChatbotGateway } from "./chatbot.gateway";
|
||||
import { ChatMessage } from "./entities/chat-message.entity";
|
||||
import { ChatSession } from "./entities/chat-session.entity";
|
||||
import { WebSocketAuthGuard } from "./guards/websocket-auth.guard";
|
||||
import { ChatbotService } from "./providers/chatbot.service";
|
||||
import { DataContextService } from "./providers/data-context.service";
|
||||
import { LangChainService } from "./providers/langchain.service";
|
||||
import { LLMService } from "./providers/llm.service";
|
||||
import { WebSocketAuthService } from "./providers/websocket-auth.service";
|
||||
import { AuthModule } from "../auth/auth.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([ChatSession, ChatMessage]), AuthModule, UsersModule],
|
||||
controllers: [ChatbotController],
|
||||
providers: [ChatbotService, ChatbotGateway, LLMService, LangChainService, DataContextService, WebSocketAuthGuard, WebSocketAuthService],
|
||||
exports: [ChatbotService, ChatbotGateway],
|
||||
})
|
||||
export class ChatbotModule {}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Entity, EntityRepositoryType, Enum, ManyToOne, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { ChatSession } from "./chat-session.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { ChatMessageRepository } from "../repositories/chat-message.repository";
|
||||
|
||||
export enum MessageType {
|
||||
USER = "user",
|
||||
BOT = "bot",
|
||||
SYSTEM = "system",
|
||||
}
|
||||
|
||||
export enum MessageStatus {
|
||||
SENT = "sent",
|
||||
DELIVERED = "delivered",
|
||||
READ = "read",
|
||||
FAILED = "failed",
|
||||
}
|
||||
|
||||
@Entity({ repository: () => ChatMessageRepository })
|
||||
export class ChatMessage extends BaseEntity {
|
||||
@Property({ type: "text" })
|
||||
content!: string;
|
||||
|
||||
@Enum({ items: () => MessageType, nativeEnumName: "message_type" })
|
||||
type!: MessageType;
|
||||
|
||||
@Enum({ items: () => MessageStatus, nativeEnumName: "message_status", default: MessageStatus.SENT })
|
||||
status: MessageStatus & Opt;
|
||||
|
||||
@ManyToOne(() => ChatSession)
|
||||
session!: ChatSession;
|
||||
|
||||
@ManyToOne(() => User, { nullable: true })
|
||||
sender?: User;
|
||||
|
||||
@Property({ type: "json", nullable: true })
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
@Property({ nullable: true })
|
||||
responseToId?: string;
|
||||
|
||||
@Property({ default: 0 })
|
||||
tokensUsed: number = 0;
|
||||
|
||||
[EntityRepositoryType]?: ChatMessageRepository;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Collection, Entity, EntityRepositoryType, Enum, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { ChatMessage } from "./chat-message.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { ChatSessionRepository } from "../repositories/chat-session.repository";
|
||||
|
||||
export enum ChatSessionStatus {
|
||||
ACTIVE = "active",
|
||||
CLOSED = "closed",
|
||||
ARCHIVED = "archived",
|
||||
}
|
||||
|
||||
@Entity({ repository: () => ChatSessionRepository })
|
||||
export class ChatSession extends BaseEntity {
|
||||
@Property({ type: "varchar" })
|
||||
title!: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
user!: User;
|
||||
|
||||
@Enum({ items: () => ChatSessionStatus, nativeEnumName: "chat_session_status", default: ChatSessionStatus.ACTIVE })
|
||||
status: ChatSessionStatus & Opt;
|
||||
|
||||
@OneToMany(() => ChatMessage, (chatMessage) => chatMessage.session)
|
||||
messages = new Collection<ChatMessage>(this);
|
||||
|
||||
@Property({ type: "json", nullable: true })
|
||||
context?: Record<string, any>;
|
||||
|
||||
@Property({ nullable: true })
|
||||
lastMessageAt?: Date;
|
||||
|
||||
[EntityRepositoryType]?: ChatSessionRepository;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, Logger } from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { WsException } from "@nestjs/websockets";
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
import { WebSocketMessage } from "../../../common/enums/message.enum";
|
||||
import { WEBSOCKET_EVENTS } from "../constants/chatbot.constants";
|
||||
|
||||
@Injectable()
|
||||
export class WebSocketAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(WebSocketAuthGuard.name);
|
||||
|
||||
constructor(private jwtService: JwtService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const client: Socket = context.switchToWs().getClient<Socket>();
|
||||
|
||||
try {
|
||||
// Get token from auth object or handshake query
|
||||
const token = this.extractTokenFromHeader(client);
|
||||
|
||||
if (!token) {
|
||||
this.logger.warn("No token provided in WebSocket connection");
|
||||
throw new WsException({
|
||||
event: WEBSOCKET_EVENTS.UNAUTHORIZED,
|
||||
message: WebSocketMessage.AUTHENTICATION_REQUIRED,
|
||||
});
|
||||
}
|
||||
|
||||
// Verify JWT token
|
||||
const payload = await this.jwtService.verifyAsync(token);
|
||||
|
||||
client.data.user = payload;
|
||||
this.logger.log(`WebSocket authentication successful for user: ${payload.id}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error("WebSocket authentication failed:", error);
|
||||
|
||||
client.emit(WEBSOCKET_EVENTS.UNAUTHORIZED, {
|
||||
message: WebSocketMessage.INVALID_TOKEN,
|
||||
});
|
||||
|
||||
throw new WsException({
|
||||
event: WEBSOCKET_EVENTS.UNAUTHORIZED,
|
||||
message: WebSocketMessage.INVALID_TOKEN,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(client: Socket): string | undefined {
|
||||
const authToken = client.handshake?.auth?.token || client.handshake?.query?.token;
|
||||
return authToken;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
|
||||
|
||||
/**
|
||||
* Extended Socket interface with authenticated user data
|
||||
*/
|
||||
export interface AuthenticatedSocket extends Socket {
|
||||
data: {
|
||||
user: ITokenPayload;
|
||||
user: { id: string; sub: string };
|
||||
sessionId?: string;
|
||||
};
|
||||
}
|
||||
@@ -47,6 +45,6 @@ export interface WebSocketResponse<T = any> {
|
||||
*/
|
||||
export interface AuthenticationResult {
|
||||
success: boolean;
|
||||
user?: ITokenPayload;
|
||||
user?: { id: string; sub: string };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const chatMessageSchema = new Schema<IChatMessage>(
|
||||
chatMessageSchema.index({ session: 1, createdAt: -1 });
|
||||
chatMessageSchema.index({ session: 1, createdAt: 1 });
|
||||
|
||||
const ChatMessageModel = model<IChatMessage>("ChatMessage", chatMessageSchema);
|
||||
const ChatMessageModel = model<IChatMessage>("ChatbotMessage", chatMessageSchema);
|
||||
|
||||
export { ChatMessageModel, MessageStatus, MessageType };
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { isValidObjectId } from "mongoose";
|
||||
|
||||
import { LangChainService } from "./langchain.service";
|
||||
import { LLMService } from "./llm.service";
|
||||
import { UserModel } from "../../user/models/user.model";
|
||||
import { CreateChatSessionDto } from "../DTO/create-chat-session.dto";
|
||||
@@ -17,26 +16,17 @@ import { BadRequestError, NotFoundError } from "../../../core/app/app.errors";
|
||||
import { Logger } from "../../../core/logging/logger";
|
||||
import { IOCTYPES } from "../../../IOC/ioc.types";
|
||||
|
||||
export enum LLMProvider {
|
||||
LANGCHAIN = "langchain",
|
||||
GEMINI = "gemini",
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ChatbotService {
|
||||
private readonly logger: Logger;
|
||||
private defaultProvider: LLMProvider;
|
||||
|
||||
constructor(
|
||||
@inject(IOCTYPES.ChatbotLLMService) private llmService: LLMService,
|
||||
@inject(IOCTYPES.ChatbotLangChainService) private langChainService: LangChainService,
|
||||
@inject(IOCTYPES.ChatSessionRepository) private chatSessionRepo: ChatSessionRepository,
|
||||
@inject(IOCTYPES.ChatMessageRepository) private chatMessageRepo: ChatMessageRepository,
|
||||
@inject(IOCTYPES.ChatbotChatSessionRepository) private chatSessionRepo: ChatSessionRepository,
|
||||
@inject(IOCTYPES.ChatbotChatMessageRepository) private chatMessageRepo: ChatMessageRepository,
|
||||
) {
|
||||
this.logger = new Logger("ChatbotService");
|
||||
// Configure default provider - LangChain for enhanced responses
|
||||
this.defaultProvider = process.env.DEFAULT_LLM_PROVIDER === "gemini" ? LLMProvider.GEMINI : LLMProvider.LANGCHAIN;
|
||||
this.logger.info(`Using ${this.defaultProvider} as default LLM provider`);
|
||||
this.logger.info("Using Google Gemini as LLM provider");
|
||||
}
|
||||
|
||||
async createChatSession(userId: string, createDto: CreateChatSessionDto) {
|
||||
@@ -82,24 +72,7 @@ export class ChatbotService {
|
||||
return this.mapSessionToDto(session);
|
||||
}
|
||||
|
||||
async sendMessage(userId: string, sendDto: SendMessageDto, provider?: LLMProvider) {
|
||||
const selectedProvider = provider || this.defaultProvider;
|
||||
|
||||
// Use LangChain by default for enhanced responses
|
||||
if (selectedProvider === LLMProvider.LANGCHAIN) {
|
||||
try {
|
||||
return await this.sendMessageWithLangChain(userId, sendDto);
|
||||
} catch (error) {
|
||||
this.logger.error("LangChain failed, falling back to Gemini", error);
|
||||
// Fallback to Gemini service
|
||||
return await this.sendMessageWithGemini(userId, sendDto);
|
||||
}
|
||||
} else {
|
||||
return await this.sendMessageWithGemini(userId, sendDto);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMessageWithGemini(userId: string, sendDto: SendMessageDto) {
|
||||
async sendMessage(userId: string, sendDto: SendMessageDto) {
|
||||
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
|
||||
throw new BadRequestError("Invalid session or user ID");
|
||||
}
|
||||
@@ -137,24 +110,7 @@ export class ChatbotService {
|
||||
return this.mapMessageToDto(userMessage);
|
||||
}
|
||||
|
||||
async sendMessageStream(userId: string, sendDto: SendMessageDto, provider?: LLMProvider) {
|
||||
const selectedProvider = provider || this.defaultProvider;
|
||||
|
||||
// Use LangChain by default for enhanced streaming responses
|
||||
if (selectedProvider === LLMProvider.LANGCHAIN) {
|
||||
try {
|
||||
return await this.sendMessageStreamWithLangChain(userId, sendDto);
|
||||
} catch (error) {
|
||||
this.logger.error("LangChain streaming failed, falling back to Gemini", error);
|
||||
// Fallback to Gemini service
|
||||
return await this.sendMessageStreamWithGemini(userId, sendDto);
|
||||
}
|
||||
} else {
|
||||
return await this.sendMessageStreamWithGemini(userId, sendDto);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMessageStreamWithGemini(userId: string, sendDto: SendMessageDto) {
|
||||
async sendMessageStream(userId: string, sendDto: SendMessageDto) {
|
||||
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
|
||||
throw new BadRequestError("Invalid session or user ID");
|
||||
}
|
||||
@@ -202,7 +158,7 @@ export class ChatbotService {
|
||||
userPreferences: session.context,
|
||||
};
|
||||
|
||||
// Return a function that generates the stream using original LLM service
|
||||
// Return a function that generates the stream using LLM service
|
||||
const streamGenerator = () => this.llmService.generateStreamResponse(sendDto.content, context);
|
||||
|
||||
return {
|
||||
@@ -243,7 +199,7 @@ export class ChatbotService {
|
||||
const llmResponse = await this.llmService.generateResponse(userMessage.content, context);
|
||||
|
||||
// Create bot message
|
||||
const botMessage = await ChatMessageModel.create({
|
||||
await ChatMessageModel.create({
|
||||
content: llmResponse.message,
|
||||
type: MessageType.BOT,
|
||||
session: sessionId,
|
||||
@@ -334,169 +290,4 @@ export class ChatbotService {
|
||||
};
|
||||
}
|
||||
|
||||
async sendMessageWithLangChain(userId: string, sendDto: SendMessageDto) {
|
||||
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
|
||||
throw new BadRequestError("Invalid session or user ID");
|
||||
}
|
||||
|
||||
const session = await ChatSessionModel.findOne({ _id: sendDto.sessionId, user: userId }).lean();
|
||||
if (!session) {
|
||||
throw new NotFoundError("Chat session not found");
|
||||
}
|
||||
|
||||
if (session.status !== ChatSessionStatus.ACTIVE) {
|
||||
throw new BadRequestError("Cannot send message to inactive session");
|
||||
}
|
||||
|
||||
const user = await UserModel.findById(userId);
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
|
||||
// Create user message
|
||||
const userMessage = await ChatMessageModel.create({
|
||||
content: sendDto.content,
|
||||
type: MessageType.USER,
|
||||
session: sendDto.sessionId,
|
||||
sender: userId,
|
||||
responseToId: sendDto.responseToId,
|
||||
metadata: sendDto.metadata,
|
||||
});
|
||||
|
||||
// Update session last message time
|
||||
await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId);
|
||||
|
||||
// Generate bot response using LangChain
|
||||
this.generateBotResponseWithLangChain(sendDto.sessionId, userMessage._id.toString(), userId);
|
||||
|
||||
return this.mapMessageToDto(userMessage);
|
||||
}
|
||||
|
||||
async sendMessageStreamWithLangChain(userId: string, sendDto: SendMessageDto) {
|
||||
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
|
||||
throw new BadRequestError("Invalid session or user ID");
|
||||
}
|
||||
|
||||
const session = await ChatSessionModel.findOne({ _id: sendDto.sessionId, user: userId }).lean();
|
||||
if (!session) {
|
||||
throw new NotFoundError("Chat session not found");
|
||||
}
|
||||
|
||||
if (session.status !== ChatSessionStatus.ACTIVE) {
|
||||
throw new BadRequestError("Cannot send message to inactive session");
|
||||
}
|
||||
|
||||
const user = await UserModel.findById(userId);
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
|
||||
// Create user message
|
||||
const userMessage = await ChatMessageModel.create({
|
||||
content: sendDto.content,
|
||||
type: MessageType.USER,
|
||||
session: sendDto.sessionId,
|
||||
sender: userId,
|
||||
responseToId: sendDto.responseToId,
|
||||
metadata: sendDto.metadata,
|
||||
});
|
||||
|
||||
// Update session last message time
|
||||
await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId);
|
||||
|
||||
// Get conversation history for context
|
||||
const history = await this.chatMessageRepo.getConversationHistory(sendDto.sessionId, 20);
|
||||
|
||||
// Build context for LangChain
|
||||
const context: IChatContext = {
|
||||
userId,
|
||||
sessionId: sendDto.sessionId,
|
||||
conversationHistory: history.reverse().map((msg) => ({
|
||||
content: msg.content,
|
||||
type: msg.type as "user" | "bot" | "system",
|
||||
timestamp: msg.createdAt,
|
||||
metadata: msg.metadata,
|
||||
})),
|
||||
userPreferences: session.context,
|
||||
};
|
||||
|
||||
// Return a function that generates the stream using LangChain
|
||||
const streamGenerator = () => this.langChainService.generateStreamResponse(sendDto.content, context);
|
||||
|
||||
return {
|
||||
userMessage: this.mapMessageToDto(userMessage),
|
||||
streamGenerator,
|
||||
};
|
||||
}
|
||||
|
||||
private async generateBotResponseWithLangChain(sessionId: string, userMessageId: string, userId: string): Promise<void> {
|
||||
try {
|
||||
// Get conversation history
|
||||
const history = await this.chatMessageRepo.getConversationHistory(sessionId, 20);
|
||||
|
||||
const session = await ChatSessionModel.findById(sessionId).lean();
|
||||
if (!session) {
|
||||
throw new NotFoundError("Session not found");
|
||||
}
|
||||
|
||||
// Build context for LangChain
|
||||
const context: IChatContext = {
|
||||
userId,
|
||||
sessionId,
|
||||
conversationHistory: history.reverse().map((msg) => ({
|
||||
content: msg.content,
|
||||
type: msg.type as "user" | "bot" | "system",
|
||||
timestamp: msg.createdAt,
|
||||
metadata: msg.metadata,
|
||||
})),
|
||||
userPreferences: session.context,
|
||||
};
|
||||
|
||||
const userMessage = await ChatMessageModel.findById(userMessageId);
|
||||
if (!userMessage) {
|
||||
throw new NotFoundError("User message not found");
|
||||
}
|
||||
|
||||
// Generate response using LangChain
|
||||
const langChainResponse = await this.langChainService.generateResponse(userMessage.content, context);
|
||||
|
||||
// Create bot message
|
||||
const botMessage = await ChatMessageModel.create({
|
||||
content: langChainResponse.message,
|
||||
type: MessageType.BOT,
|
||||
session: sessionId,
|
||||
responseToId: userMessageId,
|
||||
tokensUsed: langChainResponse.tokensUsed,
|
||||
metadata: {
|
||||
confidence: langChainResponse.confidence,
|
||||
sources: langChainResponse.sources,
|
||||
llmContext: langChainResponse.context,
|
||||
provider: "langchain",
|
||||
documentsRetrieved: langChainResponse.context?.documentsRetrieved || 0,
|
||||
},
|
||||
});
|
||||
|
||||
// Update session last message time
|
||||
await this.chatSessionRepo.updateLastMessageTime(sessionId);
|
||||
|
||||
this.logger.info(`Generated LangChain bot response for session ${sessionId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to generate LangChain bot response for session ${sessionId}`, error);
|
||||
|
||||
// Fallback to regular LLM service
|
||||
this.logger.info(`Falling back to regular LLM service for session ${sessionId}`);
|
||||
await this.generateBotResponse(sessionId, userMessageId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshLangChainData() {
|
||||
try {
|
||||
await this.langChainService.refreshVectorStore();
|
||||
this.logger.info("LangChain vector store refreshed successfully");
|
||||
return { message: "LangChain vector store refreshed successfully" };
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to refresh LangChain vector store", error);
|
||||
throw new Error("Failed to refresh training data");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ export class DataContextService {
|
||||
this.logger = new Logger("DataContextService");
|
||||
}
|
||||
|
||||
async getRelevantContext(message: string, context: IChatContext): Promise<{ data: string[]; sources: string[] }> {
|
||||
async getRelevantContext(_message: string, _context: IChatContext): Promise<{ data: string[]; sources: string[] }> {
|
||||
const relevantData: string[] = [];
|
||||
const sources: string[] = [];
|
||||
|
||||
try {
|
||||
// Analyze message to determine what data to fetch
|
||||
const keywords = this.extractKeywords(message);
|
||||
// const keywords = this._extractKeywords(_message); // TODO: Use when implementing keyword-based filtering
|
||||
|
||||
// TODO: Implement actual data fetching based on your project's models
|
||||
// For now, this returns empty data - you can extend this to fetch:
|
||||
@@ -47,29 +47,33 @@ export class DataContextService {
|
||||
}
|
||||
}
|
||||
|
||||
private extractKeywords(message: string): string[] {
|
||||
// TODO: Use this method when implementing keyword-based filtering
|
||||
// @ts-expect-error - Method reserved for future use
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private _extractKeywords(_message: string): string[] {
|
||||
const commonWords = ["the", "is", "at", "which", "on", "a", "an", "and", "or", "but", "in", "with", "to", "for", "of", "as", "by"];
|
||||
|
||||
return message
|
||||
return _message
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s]/g, "")
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 2 && !commonWords.includes(word));
|
||||
.filter((word: string) => word.length > 2 && !commonWords.includes(word));
|
||||
}
|
||||
|
||||
// Helper methods for keyword detection - can be extended
|
||||
private hasProductKeywords(keywords: string[]): boolean {
|
||||
const productTerms = ["product", "item", "goods", "merchandise", "کالا", "محصول"];
|
||||
return keywords.some((keyword) => productTerms.includes(keyword));
|
||||
}
|
||||
// TODO: Implement when needed
|
||||
// private hasProductKeywords(keywords: string[]): boolean {
|
||||
// const productTerms = ["product", "item", "goods", "merchandise", "کالا", "محصول"];
|
||||
// return keywords.some(keyword => productTerms.includes(keyword));
|
||||
// }
|
||||
|
||||
private hasOrderKeywords(keywords: string[]): boolean {
|
||||
const orderTerms = ["order", "purchase", "buy", "cart", "سفارش", "خرید"];
|
||||
return keywords.some((keyword) => orderTerms.includes(keyword));
|
||||
}
|
||||
// private hasOrderKeywords(keywords: string[]): boolean {
|
||||
// const orderTerms = ["order", "purchase", "buy", "cart", "سفارش", "خرید"];
|
||||
// return keywords.some(keyword => orderTerms.includes(keyword));
|
||||
// }
|
||||
|
||||
private hasCategoryKeywords(keywords: string[]): boolean {
|
||||
const categoryTerms = ["category", "type", "kind", "دسته", "دستهبندی"];
|
||||
return keywords.some((keyword) => categoryTerms.includes(keyword));
|
||||
}
|
||||
// private hasCategoryKeywords(keywords: string[]): boolean {
|
||||
// const categoryTerms = ["category", "type", "kind", "دسته", "دستهبندی"];
|
||||
// return keywords.some(keyword => categoryTerms.includes(keyword));
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import { StringOutputParser } from "@langchain/core/output_parsers";
|
||||
import { PromptTemplate } from "@langchain/core/prompts";
|
||||
import { RunnableSequence } from "@langchain/core/runnables";
|
||||
import { ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
|
||||
import { injectable } from "inversify";
|
||||
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
|
||||
import { MemoryVectorStore } from "langchain/vectorstores/memory";
|
||||
|
||||
import { CHATBOT_CONSTANTS } from "../constants/chatbot.constants";
|
||||
import { IChatContext, IChatbotResponse } from "../interfaces/chatbot.interface";
|
||||
import { Logger } from "../../../core/logging/logger";
|
||||
|
||||
@injectable()
|
||||
export class LangChainService {
|
||||
private readonly logger: Logger;
|
||||
private embeddings: GoogleGenerativeAIEmbeddings;
|
||||
private llm: ChatGoogleGenerativeAI;
|
||||
private vectorStore: MemoryVectorStore;
|
||||
private textSplitter: RecursiveCharacterTextSplitter;
|
||||
private isInitialized = false;
|
||||
|
||||
constructor() {
|
||||
this.logger = new Logger("LangChainService");
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("GEMINI_API_KEY is required");
|
||||
}
|
||||
|
||||
// Initialize Google Gemini components
|
||||
this.embeddings = new GoogleGenerativeAIEmbeddings({
|
||||
apiKey,
|
||||
modelName: "embedding-001", // Google's embedding model
|
||||
});
|
||||
|
||||
this.llm = new ChatGoogleGenerativeAI({
|
||||
apiKey,
|
||||
model: CHATBOT_CONSTANTS.DEFAULT_MODEL,
|
||||
temperature: CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE,
|
||||
maxOutputTokens: CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS,
|
||||
});
|
||||
|
||||
this.textSplitter = new RecursiveCharacterTextSplitter({
|
||||
chunkSize: 1000,
|
||||
chunkOverlap: 200,
|
||||
separators: ["\n\n", "\n", ".", "!", "?", "؟", "!", ".", " ", ""],
|
||||
});
|
||||
|
||||
// Initialize vector store asynchronously
|
||||
this.initializeVectorStore().catch((error) => {
|
||||
this.logger.error("Failed to initialize vector store", error);
|
||||
});
|
||||
}
|
||||
|
||||
private async initializeVectorStore() {
|
||||
try {
|
||||
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_DATA);
|
||||
|
||||
// Load documents for training
|
||||
// TODO: Load actual data from your database models (Product, Category, etc.)
|
||||
const documents = await this.loadDocuments();
|
||||
|
||||
if (documents.length === 0) {
|
||||
this.logger.warn(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.NO_DATA_FOUND);
|
||||
this.vectorStore = new MemoryVectorStore(this.embeddings);
|
||||
this.isInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Split documents into chunks
|
||||
const splitDocs = await this.textSplitter.splitDocuments(documents);
|
||||
|
||||
this.logger.info(
|
||||
CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.DOCUMENTS_SPLIT.replace("{totalDocs}", documents.length.toString()).replace(
|
||||
"{chunks}",
|
||||
splitDocs.length.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
// Create vector store from documents
|
||||
this.vectorStore = await MemoryVectorStore.fromDocuments(splitDocs, this.embeddings);
|
||||
|
||||
this.isInitialized = true;
|
||||
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.VECTOR_STORE_READY);
|
||||
} catch (error) {
|
||||
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.VECTOR_STORE_ERROR, error);
|
||||
// Fallback to empty vector store
|
||||
this.vectorStore = new MemoryVectorStore(this.embeddings);
|
||||
this.isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadDocuments(): Promise<Document[]> {
|
||||
const documents: Document[] = [];
|
||||
|
||||
try {
|
||||
// TODO: Load actual data from your database
|
||||
// Example:
|
||||
// const products = await ProductModel.find({ ... }).limit(100);
|
||||
// products.forEach((product) => {
|
||||
// documents.push(
|
||||
// new Document({
|
||||
// pageContent: `Product: ${product.title_fa} - Description: ${product.description}`,
|
||||
// metadata: { type: "product", id: product._id.toString() },
|
||||
// }),
|
||||
// );
|
||||
// });
|
||||
|
||||
// Add comprehensive Farsi guidance documents from constants
|
||||
const companyGuidanceDocuments = [
|
||||
new Document({
|
||||
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.COMPANY_GUIDE,
|
||||
metadata: {
|
||||
type: "company_guide",
|
||||
category: "guidance",
|
||||
language: "فارسی",
|
||||
},
|
||||
}),
|
||||
new Document({
|
||||
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.COMPANY_FAQ,
|
||||
metadata: {
|
||||
type: "company_faq",
|
||||
category: "support",
|
||||
language: "فارسی",
|
||||
},
|
||||
}),
|
||||
new Document({
|
||||
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.SEARCH_GUIDE,
|
||||
metadata: {
|
||||
type: "search_guide",
|
||||
category: "tutorial",
|
||||
language: "فارسی",
|
||||
},
|
||||
}),
|
||||
new Document({
|
||||
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.INDUSTRY_GUIDE,
|
||||
metadata: {
|
||||
type: "industry_guide",
|
||||
category: "education",
|
||||
language: "فارسی",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
documents.push(...companyGuidanceDocuments);
|
||||
|
||||
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.DOCUMENTS_LOADED.replace("{count}", documents.length.toString()));
|
||||
return documents;
|
||||
} catch (error) {
|
||||
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_ERROR, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async generateResponse(message: string, context: IChatContext): Promise<IChatbotResponse> {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.SERVICE_NOT_INITIALIZED);
|
||||
}
|
||||
|
||||
try {
|
||||
// Retrieve relevant documents based on the user's query
|
||||
const relevantDocs = await this.vectorStore.similaritySearch(message, 5);
|
||||
|
||||
// Build context from retrieved documents
|
||||
const retrievedContext = relevantDocs.map((doc, index) => `${index + 1}. ${doc.pageContent}`).join("\n\n");
|
||||
|
||||
// Build conversation history
|
||||
const conversationHistory =
|
||||
context.conversationHistory
|
||||
?.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY / 4)
|
||||
?.map((msg) => {
|
||||
const roleLabel = msg.type === "user" ? "کاربر" : msg.type === "bot" ? "ربات" : "سیستم";
|
||||
return `${roleLabel}: ${msg.content}`;
|
||||
})
|
||||
?.join("\n") || "";
|
||||
|
||||
// Create the prompt template using constants
|
||||
const promptTemplate = PromptTemplate.fromTemplate(CHATBOT_CONSTANTS.COMPANY_GUIDANCE_SYSTEM_PROMPT);
|
||||
|
||||
// Create the runnable sequence
|
||||
const chain = RunnableSequence.from([promptTemplate, this.llm, new StringOutputParser()]);
|
||||
|
||||
// Execute the chain
|
||||
const response = await chain.invoke({
|
||||
context: retrievedContext,
|
||||
conversation_history: conversationHistory,
|
||||
question: message,
|
||||
});
|
||||
|
||||
// Calculate approximate token usage
|
||||
const tokensUsed = this.estimateTokens(retrievedContext) + this.estimateTokens(message) + this.estimateTokens(response);
|
||||
|
||||
return {
|
||||
message: response,
|
||||
confidence: 0.9, // Higher confidence for company guidance
|
||||
sources: relevantDocs.map((doc) => {
|
||||
const type = doc.metadata.type || "شرکت";
|
||||
return CHATBOT_CONSTANTS.SOURCE_TYPE_LABELS[type as keyof typeof CHATBOT_CONSTANTS.SOURCE_TYPE_LABELS] || type;
|
||||
}),
|
||||
tokensUsed,
|
||||
context: {
|
||||
model: `${CHATBOT_CONSTANTS.DEFAULT_MODEL}-company-guide`,
|
||||
relevantDataFound: relevantDocs.length > 0,
|
||||
documentsRetrieved: relevantDocs.length,
|
||||
vectorStoreInitialized: this.isInitialized,
|
||||
language: "فارسی",
|
||||
dataTypes: relevantDocs.map((doc) => doc.metadata.type).filter((type, index, arr) => arr.indexOf(type) === index),
|
||||
focus: "company_and_industry_guidance",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.RESPONSE_ERROR, error);
|
||||
throw new Error(CHATBOT_CONSTANTS.ERROR_MESSAGES.LLM_SERVICE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
async generateStreamResponse(message: string, context: IChatContext) {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.SERVICE_NOT_INITIALIZED);
|
||||
}
|
||||
|
||||
try {
|
||||
// Retrieve relevant documents
|
||||
const relevantDocs = await this.vectorStore.similaritySearch(message, 5);
|
||||
const retrievedContext = relevantDocs.map((doc, index) => `${index + 1}. ${doc.pageContent}`).join("\n\n");
|
||||
|
||||
// Build conversation history
|
||||
const conversationHistory =
|
||||
context.conversationHistory
|
||||
?.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY / 4)
|
||||
?.map((msg) => {
|
||||
const roleLabel = msg.type === "user" ? "کاربر" : msg.type === "bot" ? "ربات" : "سیستم";
|
||||
return `${roleLabel}: ${msg.content}`;
|
||||
})
|
||||
?.join("\n") || "";
|
||||
|
||||
// Create prompt template using constants
|
||||
const promptTemplate = PromptTemplate.fromTemplate(CHATBOT_CONSTANTS.COMPANY_GUIDANCE_SYSTEM_PROMPT);
|
||||
|
||||
// Create streaming chain
|
||||
const chain = RunnableSequence.from([promptTemplate, this.llm, new StringOutputParser()]);
|
||||
|
||||
// Return async iterable for streaming
|
||||
const stream = await chain.stream({
|
||||
context: retrievedContext,
|
||||
conversation_history: conversationHistory,
|
||||
question: message,
|
||||
});
|
||||
|
||||
return stream;
|
||||
} catch (error) {
|
||||
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.STREAM_RESPONSE_ERROR, error);
|
||||
throw new Error(CHATBOT_CONSTANTS.ERROR_MESSAGES.LLM_SERVICE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshVectorStore(): Promise<void> {
|
||||
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.REFRESHING_VECTOR_STORE);
|
||||
this.isInitialized = false;
|
||||
await this.initializeVectorStore();
|
||||
}
|
||||
|
||||
private estimateTokens(text: string): number {
|
||||
// Rough estimation: 1 token ≈ 4 characters for most languages
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
get initialized(): boolean {
|
||||
return this.isInitialized;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { inject, injectable } from "inversify";
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
import { WebSocketMessage } from "../../../common/enums/message.enum";
|
||||
import { AuthTokenPayload } from "../../../common/types/jwt.type";
|
||||
import { jwtExpiredErr } from "../../../core/app/app.errors";
|
||||
import { Logger } from "../../../core/logging/logger";
|
||||
import { IOCTYPES } from "../../../IOC/ioc.types";
|
||||
|
||||
Reference in New Issue
Block a user