diff --git a/package.json b/package.json index 6cbc319..feda870 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47dd938..5122a56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/IOC/ioc.config.ts b/src/IOC/ioc.config.ts index 3ca7c18..aa3dce7 100644 --- a/src/IOC/ioc.config.ts +++ b/src/IOC/ioc.config.ts @@ -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(IOCTYPES.CacheService).to(CacheService).inSingletonScope(); bind(IOCTYPES.ChatbotService).to(ChatbotService).inSingletonScope(); bind(IOCTYPES.ChatbotLLMService).to(LLMService).inSingletonScope(); - bind(IOCTYPES.ChatbotLangChainService).to(LangChainService).inSingletonScope(); bind(IOCTYPES.ChatbotDataContextService).to(DataContextService).inSingletonScope(); bind(IOCTYPES.ChatbotWebSocketAuthService).to(WebSocketAuthService).inSingletonScope(); // #endregion @@ -291,8 +289,8 @@ const containerModules = new AsyncContainerModule(async (bind) => { bind(IOCTYPES.AboutUsRepo).toDynamicValue(CreateAboutUsRepo).inSingletonScope(); bind(IOCTYPES.SiteSettingRepo).toDynamicValue(CreateSiteSettingRepo).inSingletonScope(); bind(IOCTYPES.NewsletterRepo).toDynamicValue(CreateNewsletterRepo).inSingletonScope(); - bind(IOCTYPES.ChatSessionRepository).toDynamicValue(createChatSessionRepository).inSingletonScope(); - bind(IOCTYPES.ChatMessageRepository).toDynamicValue(createChatMessageRepository).inSingletonScope(); + bind(IOCTYPES.ChatbotChatSessionRepository).toDynamicValue(createChatSessionRepository).inSingletonScope(); + bind(IOCTYPES.ChatbotChatMessageRepository).toDynamicValue(createChatMessageRepository).inSingletonScope(); // #endregion }); diff --git a/src/IOC/ioc.types.ts b/src/IOC/ioc.types.ts index 0845871..0ecd56d 100644 --- a/src/IOC/ioc.types.ts +++ b/src/IOC/ioc.types.ts @@ -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"), diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 090b233..bb4ecc6 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -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 = "سرویس هوش مصنوعی موقتاً در دسترس نیست", +} diff --git a/src/modules/chatbot/DTO/send-message.dto.ts b/src/modules/chatbot/DTO/send-message.dto.ts index 1560845..8ed0894 100644 --- a/src/modules/chatbot/DTO/send-message.dto.ts +++ b/src/modules/chatbot/DTO/send-message.dto.ts @@ -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"; diff --git a/src/modules/chatbot/DTO/session-id.param.dto.ts b/src/modules/chatbot/DTO/session-id.param.dto.ts index 6babf49..754483a 100644 --- a/src/modules/chatbot/DTO/session-id.param.dto.ts +++ b/src/modules/chatbot/DTO/session-id.param.dto.ts @@ -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; diff --git a/src/modules/chatbot/chatbot.controller.ts b/src/modules/chatbot/chatbot.controller.ts index 5fa329c..52385ce 100644 --- a/src/modules/chatbot/chatbot.controller.ts +++ b/src/modules/chatbot/chatbot.controller.ts @@ -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 { 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") diff --git a/src/modules/chatbot/chatbot.gateway.ts b/src/modules/chatbot/chatbot.gateway.ts deleted file mode 100644 index 1398d2a..0000000 --- a/src/modules/chatbot/chatbot.gateway.ts +++ /dev/null @@ -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(); - private readonly userSessions = new Map>(); - - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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(client: Socket, event: WEBSOCKET_EVENTS, data: T): void { - const response: WebSocketResponse = { - ...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 { - 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; - } -} diff --git a/src/modules/chatbot/chatbot.module.ts b/src/modules/chatbot/chatbot.module.ts deleted file mode 100644 index 43ee37b..0000000 --- a/src/modules/chatbot/chatbot.module.ts +++ /dev/null @@ -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 {} diff --git a/src/modules/chatbot/entities/chat-message.entity.ts b/src/modules/chatbot/entities/chat-message.entity.ts deleted file mode 100644 index ac5eb46..0000000 --- a/src/modules/chatbot/entities/chat-message.entity.ts +++ /dev/null @@ -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; - - @Property({ nullable: true }) - responseToId?: string; - - @Property({ default: 0 }) - tokensUsed: number = 0; - - [EntityRepositoryType]?: ChatMessageRepository; -} diff --git a/src/modules/chatbot/entities/chat-session.entity.ts b/src/modules/chatbot/entities/chat-session.entity.ts deleted file mode 100644 index 2973b44..0000000 --- a/src/modules/chatbot/entities/chat-session.entity.ts +++ /dev/null @@ -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(this); - - @Property({ type: "json", nullable: true }) - context?: Record; - - @Property({ nullable: true }) - lastMessageAt?: Date; - - [EntityRepositoryType]?: ChatSessionRepository; -} diff --git a/src/modules/chatbot/guards/websocket-auth.guard.ts b/src/modules/chatbot/guards/websocket-auth.guard.ts deleted file mode 100644 index 6e53905..0000000 --- a/src/modules/chatbot/guards/websocket-auth.guard.ts +++ /dev/null @@ -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 { - const client: Socket = context.switchToWs().getClient(); - - 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; - } -} diff --git a/src/modules/chatbot/interfaces/websocket.interface.ts b/src/modules/chatbot/interfaces/websocket.interface.ts index 326e0fd..47b850c 100644 --- a/src/modules/chatbot/interfaces/websocket.interface.ts +++ b/src/modules/chatbot/interfaces/websocket.interface.ts @@ -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 { */ export interface AuthenticationResult { success: boolean; - user?: ITokenPayload; + user?: { id: string; sub: string }; error?: string; } diff --git a/src/modules/chatbot/models/chat-message.model.ts b/src/modules/chatbot/models/chat-message.model.ts index f996c38..d954429 100644 --- a/src/modules/chatbot/models/chat-message.model.ts +++ b/src/modules/chatbot/models/chat-message.model.ts @@ -23,7 +23,7 @@ const chatMessageSchema = new Schema( chatMessageSchema.index({ session: 1, createdAt: -1 }); chatMessageSchema.index({ session: 1, createdAt: 1 }); -const ChatMessageModel = model("ChatMessage", chatMessageSchema); +const ChatMessageModel = model("ChatbotMessage", chatMessageSchema); export { ChatMessageModel, MessageStatus, MessageType }; diff --git a/src/modules/chatbot/providers/chatbot.service.ts b/src/modules/chatbot/providers/chatbot.service.ts index fdddc4d..aded06c 100644 --- a/src/modules/chatbot/providers/chatbot.service.ts +++ b/src/modules/chatbot/providers/chatbot.service.ts @@ -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 { - 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"); - } - } } diff --git a/src/modules/chatbot/providers/data-context.service.ts b/src/modules/chatbot/providers/data-context.service.ts index d8fa26d..2263a7f 100644 --- a/src/modules/chatbot/providers/data-context.service.ts +++ b/src/modules/chatbot/providers/data-context.service.ts @@ -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)); + // } } diff --git a/src/modules/chatbot/providers/langchain.service.ts b/src/modules/chatbot/providers/langchain.service.ts deleted file mode 100644 index 39c0af6..0000000 --- a/src/modules/chatbot/providers/langchain.service.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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; - } -} diff --git a/src/modules/chatbot/providers/websocket-auth.service.ts b/src/modules/chatbot/providers/websocket-auth.service.ts index 52c9ace..68595b5 100644 --- a/src/modules/chatbot/providers/websocket-auth.service.ts +++ b/src/modules/chatbot/providers/websocket-auth.service.ts @@ -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";